Rebase dev onto v2_id

This commit is contained in:
Keith Edmunds 2022-03-02 09:27:10 +00:00
parent a91309477b
commit bc6a4c11cf
4 changed files with 356 additions and 299 deletions

View File

@ -51,8 +51,10 @@ engine = sqlalchemy.create_engine(
echo=Config.DISPLAY_SQL,
pool_pre_ping=True)
sm: sessionmaker = sessionmaker(bind=engine)
Session = scoped_session(sm)
# Create a Session factory
Session = scoped_session(sessionmaker(bind=engine))
# sm: sessionmaker = sessionmaker(bind=engine) # , expire_on_commit=False)
# Session = scoped_session(sm)
Base: DeclarativeMeta = declarative_base()
Base.metadata.create_all(engine)
@ -105,7 +107,7 @@ class NoteColours(Base):
Optional["NoteColours"]:
"""Return record identified by id, or None if not found"""
return session.query(NoteColours).local_filter(
return session.query(NoteColours).filter(
NoteColours.id == note_id).first()
@staticmethod
@ -116,7 +118,7 @@ class NoteColours(Base):
for rec in (
session.query(NoteColours)
.local_filter(NoteColours.enabled.is_(True))
.filter(NoteColours.enabled.is_(True))
.order_by(NoteColours.order)
.all()
):
@ -213,7 +215,7 @@ class Playdates(Base):
"""Return datetime track last played or None"""
last_played: Optional[Playdates] = session.query(
Playdates.lastplayed).local_filter((Playdates.track_id == track_id)
Playdates.lastplayed).filter((Playdates.track_id == track_id)
).order_by(Playdates.lastplayed.desc()).first()
if last_played:
return last_played[0]
@ -226,7 +228,7 @@ class Playdates(Base):
Remove all records of track_id
"""
session.query(Playdates).local_filter(
session.query(Playdates).filter(
Playdates.track_id == track_id,
).delete()
session.commit()
@ -293,7 +295,7 @@ class Playlists(Base):
@classmethod
def get_by_id(cls, session: Session, playlist_id: int) -> "Playlists":
return (session.query(cls).local_filter(cls.id == playlist_id)).one()
return (session.query(cls).filter(cls.id == playlist_id)).one()
@classmethod
def get_closed(cls, session: Session) -> List["Playlists"]:
@ -301,7 +303,7 @@ class Playlists(Base):
return (
session.query(cls)
.local_filter(cls.loaded.is_(False))
.filter(cls.loaded.is_(False))
.order_by(cls.last_used.desc())
).all()
@ -313,7 +315,7 @@ class Playlists(Base):
return (
session.query(cls)
.local_filter(cls.loaded.is_(True))
.filter(cls.loaded.is_(True))
.order_by(cls.last_used.desc())
).all()
@ -330,7 +332,7 @@ class Playlists(Base):
Remove all tracks from this playlist
"""
session.query(PlaylistTracks).local_filter(
session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == self.id,
).delete()
session.commit()
@ -338,7 +340,7 @@ class Playlists(Base):
def remove_track(self, session: Session, row: int) -> None:
DEBUG(f"Playlist.remove_track({self.id=}, {row=})")
session.query(PlaylistTracks).local_filter(
session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == self.id,
PlaylistTracks.row == row
).delete()
@ -389,7 +391,7 @@ class PlaylistTracks(Base):
new_row: int
max_row: Optional[int] = session.query(
func.max(PlaylistTracks.row)).local_filter(
func.max(PlaylistTracks.row)).filter(
PlaylistTracks.playlist_id == to_playlist_id).scalar()
if max_row is None:
# Destination playlist is empty; use row 0
@ -398,7 +400,7 @@ class PlaylistTracks(Base):
# Destination playlist has tracks; add to end
new_row = max_row + 1
try:
record: PlaylistTracks = session.query(PlaylistTracks).local_filter(
record: PlaylistTracks = session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == from_playlist_id,
PlaylistTracks.row == row).one()
except NoResultFound:
@ -446,7 +448,7 @@ class Settings(Base):
int_setting: Settings
try:
int_setting = session.query(cls).local_filter(
int_setting = session.query(cls).filter(
cls.name == name).one()
except NoResultFound:
int_setting = Settings()
@ -516,7 +518,7 @@ class Tracks(Base):
DEBUG(f"Tracks.get_or_create({path=})")
try:
track = session.query(cls).local_filter(cls.path == path).one()
track = session.query(cls).filter(cls.path == path).one()
except NoResultFound:
track = Tracks(session, path)
@ -533,7 +535,7 @@ class Tracks(Base):
DEBUG(f"Tracks.get_track_from_filename({filename=})")
try:
track = session.query(Tracks).local_filter(Tracks.path.ilike(
track = session.query(Tracks).filter(Tracks.path.ilike(
f'%{os.path.sep}{filename}')).one()
return track
except (NoResultFound, MultipleResultsFound):
@ -547,7 +549,7 @@ class Tracks(Base):
DEBUG(f"Tracks.get_track_from_path({path=})")
return session.query(Tracks).local_filter(Tracks.path == path).first()
return session.query(Tracks).filter(Tracks.path == path).first()
@classmethod
def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]:
@ -555,7 +557,7 @@ class Tracks(Base):
try:
DEBUG(f"Tracks.get_track(track_id={track_id})")
track = session.query(Tracks).local_filter(Tracks.id == track_id).one()
track = session.query(Tracks).filter(Tracks.id == track_id).one()
return track
except NoResultFound:
ERROR(f"get_track({track_id}): not found")
@ -584,7 +586,7 @@ class Tracks(Base):
DEBUG(f"Tracks.remove_path({path=})")
try:
session.query(Tracks).local_filter(Tracks.path == path).delete()
session.query(Tracks).filter(Tracks.path == path).delete()
session.commit()
except IntegrityError as exception:
ERROR(f"Can't remove track with {path=} ({exception=})")
@ -594,7 +596,7 @@ class Tracks(Base):
return (
session.query(cls)
.local_filter(cls.artist.ilike(f"%{text}%"))
.filter(cls.artist.ilike(f"%{text}%"))
.order_by(cls.title)
).all()
@ -602,7 +604,7 @@ class Tracks(Base):
def search_titles(cls, session: Session, text: str) -> List["Tracks"]:
return (
session.query(cls)
.local_filter(cls.title.ilike(f"%{text}%"))
.filter(cls.title.ilike(f"%{text}%"))
.order_by(cls.title)
).all()

View File

@ -86,6 +86,8 @@ class Music:
p.stop()
DEBUG(f"Releasing player {p=}", True)
p.release()
# Ensure we don't reference player after release
p = None
self.fading -= 1

View File

@ -412,7 +412,8 @@ class Window(QMainWindow, Ui_MainWindow):
rows = []
for (row, track) in (
self.visible_playlist_tab().get_selected_rows_and_tracks()
self.visible_playlist_tab().get_selected_rows_and_tracks(
session)
):
rows.append(row)
if destination_visible_playlist_tab:
@ -436,8 +437,14 @@ class Window(QMainWindow, Ui_MainWindow):
Ensure we have info tabs for next and current track titles
"""
title_list: List[str, str] = [self.previous_track.title,
self.current_track.title]
title_list: List[str] = []
if self.previous_track:
title_list.append(self.previous_track.title)
if self.current_track:
title_list.append(self.current_track.title)
if self.next_track:
title_list.append(self.next_track.title)
for title in title_list:
if title in self.info_tabs.keys():
@ -533,7 +540,7 @@ class Window(QMainWindow, Ui_MainWindow):
Playdates(session, self.current_track)
# Tell playlist track is now playing
self.current_track_playlist_tab.play_started()
self.current_track_playlist_tab.play_started(session)
# Disable play next controls
self.disable_play_next_controls()
@ -680,38 +687,39 @@ 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()
with Session() as session:
# 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)
# 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,
QColor(Config.COLOUR_NORMAL_TAB))
# 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,
QColor(Config.COLOUR_NORMAL_TAB))
# Note next playlist tab
self.next_track_playlist_tab = playlist_tab
# Note next playlist tab
self.next_track_playlist_tab = playlist_tab
# 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,
QColor(Config.COLOUR_NEXT_TAB))
# 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,
QColor(Config.COLOUR_NEXT_TAB))
# Note next track
self.next_track = track
# Note next track
self.next_track = track
# Update headers
self.update_headers()
# Update headers
self.update_headers()
# Populate 'info' tabs
self.open_info_tabs()
# Populate 'info' tabs
self.open_info_tabs()
def tick(self) -> None:
"""
@ -739,52 +747,55 @@ class Window(QMainWindow, Ui_MainWindow):
# 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)
with Session() as session:
self.playing = True
if self.current_track not in session:
session.add(self.current_track)
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(self.current_track.duration)
# Elapsed time
if time_to_end < 500:
self.label_elapsed_timer.setText(
helpers.ms_to_mmss(self.current_track.duration)
)
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)
)
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))
# Time to end
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
else:
if self.playing:

View File

@ -16,6 +16,7 @@ from PyQt5.QtWidgets import (
QTableWidgetItem,
)
from sqlalchemy import inspect
from sqlalchemy.orm.exc import DetachedInstanceError
import helpers
import os
@ -192,9 +193,9 @@ class PlaylistTab(QTableWidget):
f"Moved row(s) {rows} to become row {drop_row}"
)
with Session() as session:
with Session() as session: # checked
self.save_playlist(session)
self.update_display()
self.update_display(session)
def edit(self, index, trigger, event): # review
result = super(PlaylistTab, self).edit(index, trigger, event)
@ -249,7 +250,7 @@ class PlaylistTab(QTableWidget):
def closeEvent(self, event) -> None:
"""Save column widths"""
with Session() as session:
with Session() as session: # checked
for column in range(self.columnCount()):
width = self.columnWidth(column)
name = f"playlist_col_{str(column)}_width"
@ -259,11 +260,11 @@ class PlaylistTab(QTableWidget):
event.accept()
def clear_next(self) -> None:
def clear_next(self, session) -> None:
"""Clear next track"""
self._meta_clear_next()
self.update_display()
self.update_display(session)
def create_note(self) -> None:
"""
@ -287,7 +288,7 @@ class PlaylistTab(QTableWidget):
with Session() as session:
note: Notes = Notes(
session, self.playlist.id, row, dlg.textValue())
self._insert_note(session, note, row, True)
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"""
@ -297,7 +298,7 @@ class PlaylistTab(QTableWidget):
else:
return self.selectionModel().selectedRows()[0].row()
def get_selected_rows_and_tracks(self) \
def get_selected_rows_and_tracks(self, session: Session) \
-> Optional[List[Tuple[int, Tracks]]]:
"""Return a list of selected (row-number, track) tuples"""
@ -307,7 +308,7 @@ class PlaylistTab(QTableWidget):
result = []
for row in [r.row() for r in self.selectionModel().selectedRows()]:
result.append((row, self._get_row_object(row)))
result.append((row, self._get_row_object(row, session)))
return result
@ -378,14 +379,14 @@ class PlaylistTab(QTableWidget):
self._set_row_content(row, track)
# Mark track if file is unreadable
if not self._track_is_readable(track):
if not self._file_is_readable(track.path):
self._meta_set_unreadable(row)
# Scroll to new row
self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter)
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
self.update_display(session, clear_selection=False)
def remove_rows(self, rows) -> None:
"""Remove rows passed in rows list"""
@ -398,10 +399,9 @@ class PlaylistTab(QTableWidget):
with Session() as session:
self.save_playlist(session)
self.update_display(session)
self.update_display()
def play_started(self) -> None:
def play_started(self, session: Session) -> None:
"""
Notification from musicmuster that track has started playing.
@ -425,16 +425,17 @@ class PlaylistTab(QTableWidget):
self._meta_set_played(current_row)
# Scroll to put current track in middle
scroll_to = self.item(current_row, self.COL_INDEX)
scroll_to = self.item(current_row, self.COL_MSS)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter)
# Set next track
search_from = current_row + 1
next_row = self._find_next_track_row(search_from)
self._set_next(next_row)
if next_row:
self._set_next(next_row)
# Update display
self.update_display()
self.update_display(session)
def play_stopped(self) -> None:
"""
@ -448,7 +449,6 @@ class PlaylistTab(QTableWidget):
self._meta_clear_current()
self.current_track_start_time = None
self.update_display()
def save_playlist(self, session) -> None:
"""
@ -474,7 +474,7 @@ class PlaylistTab(QTableWidget):
# PlaylistTab
for row in notes_rows:
note: Notes = self._get_row_object(row)
note: Notes = self._get_row_object(row, session)
session.add(note)
playlist_notes[note.id] = note
@ -604,9 +604,9 @@ class PlaylistTab(QTableWidget):
if row is None:
return None
self.set_next_track(row)
self._set_next(row)
def update_display(self, clear_selection: bool = True) -> None:
def update_display(self, session, clear_selection: bool = True) -> None:
"""
Set row colours, fonts, etc
@ -618,6 +618,8 @@ class PlaylistTab(QTableWidget):
- Show unplayed tracks in bold
"""
if self.playlist not in session:
session.add(self.playlist)
DEBUG(f"playlist. update_display [{self.playlist=}]")
# Clear selection if required
@ -654,120 +656,119 @@ class PlaylistTab(QTableWidget):
if not start_times_row:
start_times_row = 0
with Session() as session:
# Cycle through all rows
for row in range(self.rowCount()):
# Cycle through all rows
for row in range(self.rowCount()):
# Render notes in correct colour
if row in notes:
# Extract note text
note_text = self.item(row, self.COL_TITLE).text()
# Does the note have a start time?
row_time = self._get_note_text_time(note_text)
if row_time:
next_start_time = row_time
# Set colour
note_colour = NoteColours.get_colour(session, note_text)
if not note_colour:
note_colour = Config.COLOUR_NOTES_PLAYLIST
self._set_row_colour(
row, QColor(note_colour)
)
# Notes are always bold
self._set_row_bold(row)
# Render notes in correct colour
if row in notes:
# Extract note text
note_text = self.item(row, self.COL_TITLE).text()
# Does the note have a start time?
row_time = self._get_note_text_time(note_text)
if row_time:
next_start_time = row_time
# Set colour
note_colour = NoteColours.get_colour(session, note_text)
if not note_colour:
note_colour = Config.COLOUR_NOTES_PLAYLIST
self._set_row_colour(
row, QColor(note_colour)
)
# Notes are always bold
self._set_row_bold(row)
# Render unplayable tracks in correct colour
elif row in unreadable:
self._set_row_colour(
row, QColor(Config.COLOUR_UNREADABLE)
)
self._set_row_bold(row)
# Render unplayable tracks in correct colour
elif row in unreadable:
self._set_row_colour(
row, QColor(Config.COLOUR_UNREADABLE)
)
self._set_row_bold(row)
# Render current track
elif row == current_row:
# Set start time
self._set_row_start_time(
row, self.current_track_start_time)
# Render current track
elif row == current_row:
# Set start time
self._set_row_start_time(
row, self.current_track_start_time)
# Set last played time
last_played_str = get_relative_date(
self.current_track_start_time)
# Set last played time
last_played_str = get_relative_date(
self.current_track_start_time)
self.item(row, self.COL_LAST_PLAYED).setText(
last_played_str)
# Calculate next_start_time
track = self._get_row_object(row, session)
next_start_time = self._calculate_track_end_time(
track, self.current_track_start_time)
# Set end time
self._set_row_end_time(row, next_start_time)
# Set colour
self._set_row_colour(row, QColor(
Config.COLOUR_CURRENT_PLAYLIST))
# Make bold
self._set_row_bold(row)
# Render next track
elif row == next_row:
# if there's a track playing, set start time from that
if current_row:
start_time = self.current_track_start_time
else:
# No current track to base from, but don't change
# time if it's already set
start_time = self._get_row_start_time(row)
if not start_time:
start_time = next_start_time
self._set_row_start_time(row, start_time)
# Set end time
track = self._get_row_object(row, session)
next_start_time = self._calculate_track_end_time(
track, start_time)
self._set_row_end_time(row, next_start_time)
# Set colour
self._set_row_colour(
row, QColor(Config.COLOUR_NEXT_PLAYLIST))
# Make bold
self._set_row_bold(row)
else:
# This is a track row other than next or current
track = self._get_row_object(row, session)
if row in played:
# Played today, so update last played column
last_playedtime = Playdates.last_played(
session, track.id)
last_played_str = get_relative_date(last_playedtime)
self.item(row, self.COL_LAST_PLAYED).setText(
last_played_str)
# Calculate next_start_time
track = self._get_row_object(row)
next_start_time = self._calculate_track_end_time(
track, self.current_track_start_time)
# Set end time
self._set_row_end_time(row, next_start_time)
# Set colour
self._set_row_colour(row, QColor(
Config.COLOUR_CURRENT_PLAYLIST))
# Make bold
self._set_row_bold(row)
# Render next track
elif row == next_row:
# if there's a track playing, set start time from that
if current_row:
start_time = self.current_track_start_time
else:
# No current track to base from, but don't change
# time if it's already set
start_time = self._get_row_start_time(row)
if not start_time:
start_time = next_start_time
self._set_row_start_time(row, start_time)
# Set end time
track = self._get_row_object(row)
next_start_time = self._calculate_track_end_time(
track, start_time)
self._set_row_end_time(row, next_start_time)
# Set colour
self._set_row_colour(
row, QColor(Config.COLOUR_NEXT_PLAYLIST))
# Make bold
self._set_row_bold(row)
self._set_row_not_bold(row)
else:
# This is a track row other than next or current
track = self._get_row_object(row)
if row in played:
# Played today, so update last played column
last_playedtime = Playdates.last_played(
session, track.id)
last_played_str = get_relative_date(last_playedtime)
self.item(row, self.COL_LAST_PLAYED).setText(
last_played_str)
self._set_row_not_bold(row)
# Set start/end times as we haven't played it yet
if next_start_time and row >= start_times_row:
self._set_row_start_time(row, next_start_time)
next_start_time = self._calculate_track_end_time(
track, next_start_time)
# Set end time
self._set_row_end_time(row, next_start_time)
else:
# Set start/end times as we haven't played it yet
if next_start_time and row >= start_times_row:
self._set_row_start_time(row, next_start_time)
next_start_time = self._calculate_track_end_time(
track, next_start_time)
# Set end time
self._set_row_end_time(row, next_start_time)
else:
# Clear start and end time
self._set_row_start_time(row, None)
self._set_row_end_time(row, None)
# Don't dim unplayed tracks
self._set_row_bold(row)
# Stripe rows
if row % 2:
self._set_row_colour(
row, QColor(Config.COLOUR_ODD_PLAYLIST))
else:
self._set_row_colour(
row, QColor(Config.COLOUR_EVEN_PLAYLIST))
# Clear start and end time
self._set_row_start_time(row, None)
self._set_row_end_time(row, None)
# Don't dim unplayed tracks
self._set_row_bold(row)
# Stripe rows
if row % 2:
self._set_row_colour(
row, QColor(Config.COLOUR_ODD_PLAYLIST))
else:
self._set_row_colour(
row, QColor(Config.COLOUR_EVEN_PLAYLIST))
# ########## Internally called functions ##########
@ -779,8 +780,9 @@ class PlaylistTab(QTableWidget):
if row in self._meta_get_notes():
return None
track: Tracks = self._get_row_object(row)
open_in_audacity(track.path)
with Session() as session:
track: Tracks = self._get_row_object(row, session)
open_in_audacity(track.path)
@staticmethod
def _calculate_track_end_time(track: Tracks,
@ -811,11 +813,12 @@ class PlaylistTab(QTableWidget):
if row in self._meta_get_notes():
return None
track: Optional[Tracks] = self._get_row_object(row)
if track:
cb: QApplication.clipboard = QApplication.clipboard()
cb.clear(mode=cb.Clipboard)
cb.setText(track.path, mode=cb.Clipboard)
with Session() as session:
track: Optional[Tracks] = self._get_row_object(row, session)
if track:
cb: QApplication.clipboard = QApplication.clipboard()
cb.clear(mode=cb.Clipboard)
cb.setText(track.path, mode=cb.Clipboard)
def _cell_changed(self, row: int, column: int) -> None:
"""Called when cell content has changed"""
@ -828,8 +831,9 @@ class PlaylistTab(QTableWidget):
new_text: str = self.item(row, column).text()
DEBUG(f"_cell_changed({row=}, {column=}, {new_text=}")
row_object: Union[Tracks, Notes] = self._get_row_object(row)
with Session() as session:
row_object: Union[Tracks, Notes] = self._get_row_object(
row, session)
if row in self._meta_get_notes():
# Save change to database
DEBUG(f"Notes.update_note: saving new note text '{new_text=}'")
@ -866,7 +870,8 @@ class PlaylistTab(QTableWidget):
# update_display to update start times, such as when a note has
# been edited
self.update_display()
with Session() as session:
self.update_display(session)
self.parent.enable_play_next_controls()
@ -908,18 +913,18 @@ class PlaylistTab(QTableWidget):
if msg.exec() == QMessageBox.Yes:
rows_to_delete.append(row)
# delete in reverse row order so row numbers don't
# change
for row in sorted(rows_to_delete, reverse=True):
row_object = self._get_row_object(row)
if row in note_rows:
row_object.delete_note(session)
else:
self.remove_track(session, row)
self.removeRow(row)
# delete in reverse row order so row numbers don't
# change
for row in sorted(rows_to_delete, reverse=True):
row_object = self._get_row_object(row, session)
if row in note_rows:
row_object.delete_note(session)
else:
self.remove_track(session, row)
self.removeRow(row)
self.save_playlist(session)
self.update_display()
self.save_playlist(session)
self.update_display(session)
def _drop_on(self, event): # review
index = self.indexAt(event.pos())
@ -958,10 +963,14 @@ class PlaylistTab(QTableWidget):
except ValueError:
return None
def _get_row_object(self, row: int) -> Union[Tracks, Notes]:
def _get_row_object(self, row: int, session: Session) \
-> Union[Tracks, Notes]:
"""Return content associated with this row"""
return self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
obj = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
if obj not in session:
session.add(obj)
return obj
def _get_row_start_time(self, row: int) -> Optional[datetime]:
try:
@ -980,27 +989,29 @@ class PlaylistTab(QTableWidget):
txt: str
row_object: Union[Tracks, Notes] = self._get_row_object(row)
if row in self._meta_get_notes():
txt = row_object.note
else:
track = row_object
txt = (
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)}"
"\n\n"
f"Path: {track.path}\n"
)
info: QMessageBox = QMessageBox(self)
info.setIcon(QMessageBox.Information)
info.setText(txt)
info.setStandardButtons(QMessageBox.Ok)
info.setDefaultButton(QMessageBox.Cancel)
info.exec()
with Session() as session:
row_object: Union[Tracks, Notes] = self._get_row_object(
row, session)
if row in self._meta_get_notes():
txt = row_object.note
else:
track = row_object
txt = (
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)}"
"\n\n"
f"Path: {track.path}\n"
)
info: QMessageBox = QMessageBox(self)
info.setIcon(QMessageBox.Information)
info.setText(txt)
info.setStandardButtons(QMessageBox.Ok)
info.setDefaultButton(QMessageBox.Cancel)
info.exec()
def _insert_note(self, session: Session, note: Notes,
row: Optional[int] = None, repaint: bool = True) -> None:
@ -1040,7 +1051,7 @@ class PlaylistTab(QTableWidget):
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
self.update_display(session, clear_selection=False)
def _is_below(self, pos, index): # review
rect = self.visualRect(index)
@ -1110,6 +1121,13 @@ class PlaylistTab(QTableWidget):
current_row: Optional[int] = self._meta_get_current()
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 _meta_clear_next(self) -> None:
"""
@ -1274,7 +1292,7 @@ class PlaylistTab(QTableWidget):
# We possibly don't need to save the playlist here, but row
# numbers may have changed during population, and it's cheap to do
self.save_playlist(session)
self.update_display()
self.update_display(session)
def _rescan(self, row: int) -> None:
"""
@ -1287,11 +1305,11 @@ class PlaylistTab(QTableWidget):
if row in self._meta_get_notes():
return None
track: Tracks = self._get_row_object(row)
if track:
with Session() as session:
with Session() as session:
track: Tracks = self._get_row_object(row, session)
if track:
track.rescan(session)
self._update_row(row, track)
self._update_row(session, row, track)
def _select_event(self) -> None:
"""
@ -1304,15 +1322,19 @@ class PlaylistTab(QTableWidget):
track_rows = list(row_set - note_row_set)
tracks: List[Tracks]
tracks = [self._get_row_object(row) for row in track_rows]
ms: int = sum([track.duration for track in tracks])
with Session() as session: # checked
tracks = [self._get_row_object(row, session) for row in track_rows]
for track in tracks:
if track not in session:
session.add(track)
ms: int = sum([track.duration for track in tracks])
# Only paint message if there are selected track rows
if ms > 0:
self.parent.lblSumPlaytime.setText(
f"Selected duration: {helpers.ms_to_mmss(ms)}")
else:
self.parent.lblSumPlaytime.setText("")
# Only paint message if there are selected track rows
if ms > 0:
self.parent.lblSumPlaytime.setText(
f"Selected duration: {helpers.ms_to_mmss(ms)}")
else:
self.parent.lblSumPlaytime.setText("")
def _set_column_widths(self) -> None:
"""Column widths from settings"""
@ -1340,26 +1362,27 @@ class PlaylistTab(QTableWidget):
DEBUG(f"_set_next({row=})")
# Check row is a track row
if row in self._meta_get_notes():
return None
track: Tracks = self._get_row_object(row)
if not track:
return None
with Session() as session:
# Check row is a track row
if row in self._meta_get_notes():
return None
track: Tracks = self._get_row_object(row, session)
if not track:
return None
# Check track is readable
if not self._track_is_readable(track):
self._meta_set_unreadable(row)
return None
# Check track is readable
if not self._file_is_readable(track.path):
self._meta_set_unreadable(row)
return None
# Mark as next track
self._meta_set_next(row)
# Mark as next track
self._meta_set_next(row)
# Notify musicmuster
self.parent.this_is_the_next_track(self, track)
# Notify musicmuster
self.parent.this_is_the_next_track(self, track)
# Update display
self.update_display()
# Update display
self.update_display(session)
def _set_row_bold(self, row: int, bold: bool = True) -> None:
"""Make row bold (bold=True) or not bold"""
@ -1415,23 +1438,42 @@ class PlaylistTab(QTableWidget):
item: QTableWidgetItem = QTableWidgetItem(time_str)
self.setItem(row, self.COL_START_TIME, item)
def _track_path_is_readable(self, track_id):
def _select_tracks(self, played: bool) -> None:
"""
Select all played (played=True) or unplayed (played=False)
tracks in playlist
"""
# Need to allow multiple rows to be selected
self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
notes_rows: List[int] = self._meta_get_notes()
self.clearSelection()
played_rows: List[int] = self._meta_get_played()
for row in range(self.rowCount()):
if row in notes_rows:
continue
if row in played_rows == played:
self.selectRow(row)
# Reset extended selection
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
@staticmethod
def _file_is_readable(path: str) -> bool:
"""
Returns True if track path is readable, else False
vlc cannot read files with a colon in the path
"""
with Session() as session:
path = Tracks.get_path(session, track_id)
if os.access(path, os.R_OK):
if ':' not in path:
return True
return False
def _update_row(self, row: int, track: Tracks) -> None:
def _update_row(self, session, row: int, track: Tracks) -> None:
"""
Update the passed row with info from the passed track.
"""
@ -1454,4 +1496,4 @@ class PlaylistTab(QTableWidget):
item_duration: QTableWidgetItem = self.item(row, self.COL_DURATION)
item_duration.setText(helpers.ms_to_mmss(track.duration))
self.update_display()
self.update_display(session)