Compare commits

..

5 Commits

Author SHA1 Message Date
Keith Edmunds
3b4cf5320d Remove unused code 2022-08-15 12:45:45 +01:00
Keith Edmunds
d5950ab29a Move selected / move unplayed working 2022-08-15 12:29:36 +01:00
Keith Edmunds
eff80d684e Log exceptions to screen 2022-08-15 12:20:40 +01:00
Keith Edmunds
dcc84e0df1 Move selected working 2022-08-15 09:31:30 +01:00
Keith Edmunds
49bef912d2 Refactor playlist searching 2022-08-15 09:10:26 +01:00
6 changed files with 337 additions and 695 deletions

View File

@ -24,7 +24,8 @@ class DebugStdoutFilter(logging.Filter):
"""Filter debug messages sent to stdout""" """Filter debug messages sent to stdout"""
def filter(self, record: logging.LogRecord): def filter(self, record: logging.LogRecord):
if record.levelno != logging.DEBUG: # Exceptions are logged at ERROR level
if record.levelno in [logging.DEBUG, logging.ERROR]:
return True return True
if record.module in Config.DEBUG_MODULES: if record.module in Config.DEBUG_MODULES:
return True return True

View File

@ -66,34 +66,34 @@ class NoteColours(Base):
f"<NoteColour(id={self.id}, substring={self.substring}, " f"<NoteColour(id={self.id}, substring={self.substring}, "
f"colour={self.colour}>" f"colour={self.colour}>"
) )
#
# def __init__( # def __init__(
# self, session: Session, substring: str, colour: str, # self, session: Session, substring: str, colour: str,
# enabled: bool = True, is_regex: bool = False, # enabled: bool = True, is_regex: bool = False,
# is_casesensitive: bool = False, order: int = 0) -> None: # is_casesensitive: bool = False, order: int = 0) -> None:
# self.substring = substring # self.substring = substring
# self.colour = colour # self.colour = colour
# self.enabled = enabled # self.enabled = enabled
# self.is_regex = is_regex # self.is_regex = is_regex
# self.is_casesensitive = is_casesensitive # self.is_casesensitive = is_casesensitive
# self.order = order # self.order = order
# #
# session.add(self) # session.add(self)
# session.flush() # session.flush()
# #
# @classmethod # @classmethod
# def get_all(cls, session: Session) -> Optional[List["NoteColours"]]: # def get_all(cls, session: Session) -> Optional[List["NoteColours"]]:
# """Return all records""" # """Return all records"""
# #
# return session.query(cls).all() # return session.query(cls).all()
# #
# @classmethod # @classmethod
# def get_by_id(cls, session: Session, note_id: int) -> \ # def get_by_id(cls, session: Session, note_id: int) -> \
# Optional["NoteColours"]: # Optional["NoteColours"]:
# """Return record identified by id, or None if not found""" # """Return record identified by id, or None if not found"""
# #
# return session.query(NoteColours).filter( # return session.query(NoteColours).filter(
# NoteColours.id == note_id).first() # 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]:
@ -127,91 +127,6 @@ class NoteColours(Base):
return None return None
# class Notes(Base):
# __tablename__ = 'notes'
#
# id: int = Column(Integer, primary_key=True, autoincrement=True)
# playlist_id: int = Column(Integer, ForeignKey('playlists.id'))
# playlist: RelationshipProperty = relationship(
# "Playlists", back_populates="notes", lazy="joined")
# row: int = Column(Integer, nullable=False)
# note: str = Column(String(256), index=False)
#
# def __init__(self, session: Session, playlist_id: int,
# row: int, text: str) -> None:
# """Create note"""
#
# log.debug(f"Notes.__init__({playlist_id=}, {row=}, {text=})")
# self.playlist_id = playlist_id
# self.row = row
# self.note = text
# session.add(self)
# session.flush()
#
# def __repr__(self) -> str:
# return (
# f"<Note(id={self.id}, row={self.row}, note={self.note}>"
# )
#
# def delete_note(self, session: Session) -> None:
# """Delete note"""
#
# log.debug(f"delete_note({self.id=}")
#
# session.query(Notes).filter_by(id=self.id).delete()
# session.flush()
#
# @staticmethod
# def max_used_row(session: Session, playlist_id: int) -> Optional[int]:
# """
# Return maximum notes row for passed playlist ID or None if not notes
# """
#
# last_row = session.query(func.max(Notes.row)).filter_by(
# playlist_id=playlist_id).first()
# # if there are no rows, the above returns (None, ) which is True
# if last_row and last_row[0] is not None:
# return last_row[0]
# else:
# return None
#
# def move_row(self, session: Session, row: int, to_playlist_id: int) \
# -> None:
# """
# Move note to another playlist
# """
#
# self.row = row
# self.playlist_id = to_playlist_id
# session.commit()
#
# @classmethod
# def get_by_id(cls, session: Session, note_id: int) -> Optional["Notes"]:
# """Return note or None"""
#
# try:
# log.debug(f"Notes.get_track(track_id={note_id})")
# note = session.query(cls).filter(cls.id == note_id).one()
# return note
# except NoResultFound:
# log.error(f"get_track({note_id}): not found")
# return None
#
# def update(
# self, session: Session, row: int,
# text: Optional[str] = None) -> None:
# """
# Update note details. If text=None, don't change text.
# """
#
# log.debug(f"Notes.update_note({self.id=}, {row=}, {text=})")
#
# self.row = row
# if text:
# self.note = text
# session.flush()
class Playdates(Base): class Playdates(Base):
__tablename__ = 'playdates' __tablename__ = 'playdates'
@ -263,16 +178,16 @@ class Playdates(Base):
.scalars() .scalars()
.all() .all()
) )
#
# @staticmethod # @staticmethod
# def remove_track(session: Session, track_id: int) -> None: # def remove_track(session: Session, track_id: int) -> None:
# """ # """
# Remove all records of track_id # Remove all records of track_id
# """ # """
# #
# session.query(Playdates).filter( # session.query(Playdates).filter(
# Playdates.track_id == track_id).delete() # Playdates.track_id == track_id).delete()
# session.flush() # session.flush()
class Playlists(Base): class Playlists(Base):
@ -300,19 +215,19 @@ class Playlists(Base):
self.name = name self.name = name
session.add(self) session.add(self)
session.commit() session.commit()
#
# def add_track( # def add_track(
# self, session: Session, track_id: int, # self, session: Session, track_id: int,
# row: Optional[int] = None) -> None: # row: Optional[int] = None) -> None:
# """ # """
# Add track to playlist at given row. # Add track to playlist at given row.
# If row=None, add to end of playlist # If row=None, add to end of playlist
# """ # """
# #
# if row is None: # if row is None:
# row = self.next_free_row(session, self.id) # row = self.next_free_row(session, self.id)
# #
# xPlaylistTracks(session, self.id, track_id, row) # 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"""
@ -367,66 +282,20 @@ class Playlists(Base):
self.loaded = True self.loaded = True
self.last_used = datetime.now() self.last_used = datetime.now()
# session.flush()
# # def remove_track(self, session: Session, row: int) -> None:
# @staticmethod # log.debug(f"Playlist.remove_track({self.id=}, {row=})")
# def next_free_row(session: Session, playlist_id: int) -> int: #
# """Return next free row for this playlist""" # # Refresh self first (this is necessary when calling remove_track
# # # multiple times before session.commit())
# max_notes_row = Notes.max_used_row(session, playlist_id) # session.refresh(self)
# max_tracks_row = xPlaylistTracks.max_used_row(session, playlist_id) # # Get tracks collection for this playlist
# # # Tracks are a dictionary of tracks keyed on row
# if max_notes_row is not None and max_tracks_row is not None: # # number. Remove the relevant row.
# return max(max_notes_row, max_tracks_row) + 1 # del self.tracks[row]
# # # Save the new tracks collection
# if max_notes_row is None and max_tracks_row is None: # session.flush()
# return 0 #
#
# if max_notes_row is None:
# return max_tracks_row + 1
# else:
# return max_notes_row + 1
#
# def remove_all_tracks(self, session: Session) -> None:
# """
# Remove all tracks from this playlist
# """
#
# self.tracks = {}
# session.flush()
#
# def remove_track(self, session: Session, row: int) -> None:
# log.debug(f"Playlist.remove_track({self.id=}, {row=})")
#
# # Refresh self first (this is necessary when calling remove_track
# # multiple times before session.commit())
# session.refresh(self)
# # Get tracks collection for this playlist
# # Tracks are a dictionary of tracks keyed on row
# # number. Remove the relevant row.
# del self.tracks[row]
# # Save the new tracks collection
# session.flush()
#
#
# class PlaylistTracks(Base):
# __tablename__ = 'playlist_tracks'
#
# id: int = Column(Integer, primary_key=True, autoincrement=True)
# playlist_id: int = Column(Integer, ForeignKey('playlists.id'),
# primary_key=True)
# track_id: int = Column(Integer, ForeignKey('tracks.id'), primary_key=True)
# row: int = Column(Integer, nullable=False)
# tracks: RelationshipProperty = relationship("Tracks")
# playlist: RelationshipProperty = relationship(
# Playlists,
# backref=backref(
# "playlist_tracks",
# collection_class=attribute_mapped_collection("row"),
# lazy="joined",
# cascade="all, delete-orphan"
# )
# )
class PlaylistRows(Base): class PlaylistRows(Base):
@ -556,68 +425,33 @@ class PlaylistRows(Base):
return plrs return plrs
@staticmethod @staticmethod
def move_to_playlist(session: Session, def get_last_used_row(session: Session, playlist_id: int) -> Optional[int]:
playlistrow_ids: List[int], """Return the last used row for playlist, or None if no rows"""
destination_playlist_id: int) -> None:
"""
Move the list of playlistrow_ids to the end of destination_playlist
"""
# Find last row of destination playlist return session.execute(
last_row = session.execute(
select(func.max(PlaylistRows.row_number)) select(func.max(PlaylistRows.row_number))
.where(PlaylistRows.playlist_id == destination_playlist_id) .where(PlaylistRows.playlist_id == playlist_id)
).scalar_one() ).scalar_one()
if last_row is None:
last_row = 0
# Update the PlaylistRows entries @classmethod
for plr_id in playlistrow_ids: def get_unplayed_rows(cls, session: Session,
last_row += 1 playlist_id: int) -> List[int]:
plr = session.get(PlaylistRows, plr_id) """
plr.row_number = last_row For passed playlist, return a list of track rows that
plr.playlist_id = destination_playlist_id have not been played.
"""
session.commit() plrs = session.execute(
select(cls)
.where(
cls.playlist_id == playlist_id,
cls.track_id.is_not(None),
cls.played.is_(False)
)
.order_by(cls.row_number)
).scalars().all()
# @classmethod return plrs
# def get_playlist_rows(cls, playlist_id: int) -> \
# Optional[List["PlaylistRows"]]:
# """
# Return a list of PlaylistRows for passed playlist ordered by row
# """
#
# return session.execute(
# select(cls)
# .where(cls.playlist_id == playlist_id)
# .order_by(cls.row_number)
# ).scalars().all()
#
# @staticmethod
# def max_used_row(session: Session, playlist_id: int) -> Optional[int]:
# """
# Return highest track row number used or None if there are no
# tracks
# """
#
# last_row = session.query(
# func.max(xPlaylistTracks.row)
# ).filter_by(playlist_id=playlist_id).first()
# # if there are no rows, the above returns (None, ) which is True
# if last_row and last_row[0] is not None:
# return last_row[0]
# else:
# return None
#
# @staticmethod
# def move_row(session: Session, from_row: int, from_playlist_id: int,
# to_row: int, to_playlist_id: int) -> None:
# """Move row to another playlist"""
#
# session.query(xPlaylistTracks).filter(
# xPlaylistTracks.playlist_id == from_playlist_id,
# xPlaylistTracks.row == from_row).update(
# {'playlist_id': to_playlist_id, 'row': to_row}, False)
class Settings(Base): class Settings(Base):
@ -684,100 +518,99 @@ class Tracks(Base):
f"<Track(id={self.id}, title={self.title}, " f"<Track(id={self.id}, title={self.title}, "
f"artist={self.artist}, path={self.path}>" f"artist={self.artist}, path={self.path}>"
) )
#
# # def __init__(
# def __init__( # self,
# self, # session: Session,
# session: Session, # path: str,
# path: str, # title: Optional[str] = None,
# title: Optional[str] = None, # artist: Optional[str] = None,
# artist: Optional[str] = None, # duration: int = 0,
# duration: int = 0, # start_gap: int = 0,
# start_gap: int = 0, # fade_at: Optional[int] = None,
# fade_at: Optional[int] = None, # silence_at: Optional[int] = None,
# silence_at: Optional[int] = None, # mtime: Optional[float] = None,
# mtime: Optional[float] = None, # lastplayed: Optional[datetime] = None,
# lastplayed: Optional[datetime] = None, # ) -> None:
# ) -> None: # self.path = path
# self.path = path # self.title = title
# self.title = title # self.artist = artist
# self.artist = artist # self.duration = duration
# self.duration = duration # self.start_gap = start_gap
# self.start_gap = start_gap # self.fade_at = fade_at
# self.fade_at = fade_at # self.silence_at = silence_at
# self.silence_at = silence_at # self.mtime = mtime
# self.mtime = mtime # self.lastplayed = lastplayed
# self.lastplayed = lastplayed #
# # session.add(self)
# session.add(self) # session.flush()
# session.flush() #
# # @staticmethod
# @staticmethod # def get_all_paths(session) -> List[str]:
# def get_all_paths(session) -> List[str]: # """Return a list of paths of all tracks"""
# """Return a list of paths of all tracks""" #
# # return [a[0] for a in session.query(Tracks.path).all()]
# return [a[0] for a in session.query(Tracks.path).all()] #
# # @classmethod
# @classmethod # def get_all_tracks(cls, session: Session) -> List["Tracks"]:
# def get_all_tracks(cls, session: Session) -> List["Tracks"]: # """Return a list of all tracks"""
# """Return a list of all tracks""" #
# # return session.query(cls).all()
# return session.query(cls).all() #
# # @classmethod
# @classmethod # def get_or_create(cls, session: Session, path: str) -> "Tracks":
# def get_or_create(cls, session: Session, path: str) -> "Tracks": # """
# """ # If a track with path exists, return it;
# If a track with path exists, return it; # else created new track and return it
# else created new track and return it # """
# """ #
# # log.debug(f"Tracks.get_or_create({path=})")
# log.debug(f"Tracks.get_or_create({path=})") #
# # try:
# try: # track = session.query(cls).filter(cls.path == path).one()
# track = session.query(cls).filter(cls.path == path).one() # except NoResultFound:
# except NoResultFound: # track = Tracks(session, path)
# track = Tracks(session, path) #
# # return track
# return track #
# # @classmethod
# @classmethod # def get_by_filename(cls, session: Session, filename: str) \
# def get_by_filename(cls, session: Session, filename: str) \ # -> Optional["Tracks"]:
# -> Optional["Tracks"]: # """
# """ # Return track if one and only one track in database has passed
# Return track if one and only one track in database has passed # filename (ie, basename of path). Return None if zero or more
# filename (ie, basename of path). Return None if zero or more # than one track matches.
# than one track matches. # """
# """ #
# # log.debug(f"Tracks.get_track_from_filename({filename=})")
# log.debug(f"Tracks.get_track_from_filename({filename=})") # try:
# try: # track = session.query(Tracks).filter(Tracks.path.ilike(
# track = session.query(Tracks).filter(Tracks.path.ilike( # f'%{os.path.sep}{filename}')).one()
# f'%{os.path.sep}{filename}')).one() # return track
# return track # except (NoResultFound, MultipleResultsFound):
# except (NoResultFound, MultipleResultsFound): # return None
# return None #
# # @classmethod
# @classmethod # def get_by_path(cls, session: Session, path: str) -> List["Tracks"]:
# def get_by_path(cls, session: Session, path: str) -> List["Tracks"]: # """
# """ # Return track with passee path, or None.
# Return track with passee path, or None. # """
# """ #
# # log.debug(f"Tracks.get_track_from_path({path=})")
# log.debug(f"Tracks.get_track_from_path({path=})") #
# # return session.query(Tracks).filter(Tracks.path == path).first()
# return session.query(Tracks).filter(Tracks.path == path).first() #
# # @classmethod
# @classmethod # def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]:
# def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]: # """Return track or None"""
# """Return track or None""" #
# # try:
# try: # log.debug(f"Tracks.get_track(track_id={track_id})")
# log.debug(f"Tracks.get_track(track_id={track_id})") # track = session.query(Tracks).filter(Tracks.id == track_id).one()
# track = session.query(Tracks).filter(Tracks.id == track_id).one() # return track
# return track # except NoResultFound:
# except NoResultFound: # log.error(f"get_track({track_id}): not found")
# log.error(f"get_track({track_id}): not found") # return None
# return None
def rescan(self, session: Session) -> None: def rescan(self, session: Session) -> None:
""" """
@ -794,18 +627,18 @@ class Tracks(Base):
self.start_gap = leading_silence(audio) self.start_gap = leading_silence(audio)
session.add(self) session.add(self)
session.flush() session.flush()
#
# @staticmethod # @staticmethod
# def remove_by_path(session: Session, path: str) -> None: # def remove_by_path(session: Session, path: str) -> None:
# """Remove track with passed path from database""" # """Remove track with passed path from database"""
# #
# log.debug(f"Tracks.remove_path({path=})") # log.debug(f"Tracks.remove_path({path=})")
# #
# try: # try:
# session.query(Tracks).filter(Tracks.path == path).delete() # session.query(Tracks).filter(Tracks.path == path).delete()
# session.flush() # session.flush()
# except IntegrityError as exception: # except IntegrityError as exception:
# log.error(f"Can't remove track with {path=} ({exception=})") # log.error(f"Can't remove track with {path=} ({exception=})")
@classmethod @classmethod
def search_artists(cls, session: Session, text: str) -> List["Tracks"]: def search_artists(cls, session: Session, text: str) -> List["Tracks"]:
@ -833,26 +666,3 @@ class Tracks(Base):
.scalars() .scalars()
.all() .all()
) )
#
# @staticmethod
# def update_lastplayed(session: Session, track_id: int) -> None:
# """Update the last_played field to current datetime"""
#
# rec = session.query(Tracks).get(track_id)
# rec.lastplayed = datetime.now()
# session.add(rec)
# session.flush()
#
# def update_artist(self, session: Session, artist: str) -> None:
# self.artist = artist
# session.add(self)
# session.flush()
#
# def update_title(self, session: Session, title: str) -> None:
# self.title = title
# session.add(self)
# session.flush()
#
# def update_path(self, session, newpath: str) -> None:
# self.path = newpath
# session.commit()

View File

@ -7,6 +7,7 @@ import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
# from typing import Callable, Dict, List, Optional, Tuple # from typing import Callable, Dict, List, Optional, Tuple
from typing import List
from PyQt5.QtCore import QDate, QEvent, Qt, QTime, QTimer from PyQt5.QtCore import QDate, QEvent, Qt, QTime, QTimer
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
@ -166,7 +167,7 @@ class Window(QMainWindow, Ui_MainWindow):
# self.actionImport.triggered.connect(self.import_track) # self.actionImport.triggered.connect(self.import_track)
self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.actionInsertSectionHeader.triggered.connect(self.insert_header)
self.actionInsertTrack.triggered.connect(self.insert_track) self.actionInsertTrack.triggered.connect(self.insert_track)
# self.actionMoveSelected.triggered.connect(self.move_selected) self.actionMoveSelected.triggered.connect(self.move_selected)
self.actionNewPlaylist.triggered.connect(self.create_playlist) self.actionNewPlaylist.triggered.connect(self.create_playlist)
self.actionOpenPlaylist.triggered.connect(self.open_playlist) self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
@ -175,8 +176,7 @@ class Window(QMainWindow, Ui_MainWindow):
# self.actionSelect_played_tracks.triggered.connect(self.select_played) # self.actionSelect_played_tracks.triggered.connect(self.select_played)
self.actionSelect_previous_track.triggered.connect( self.actionSelect_previous_track.triggered.connect(
self.select_previous_row) self.select_previous_row)
# self.actionSelect_unplayed_tracks.triggered.connect( self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
# self.select_unplayed)
self.actionSetNext.triggered.connect( self.actionSetNext.triggered.connect(
lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
self.actionSkipToNext.triggered.connect(self.play_next) self.actionSkipToNext.triggered.connect(self.play_next)
@ -485,9 +485,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
playlist.mark_open(session) playlist.mark_open(session)
def move_selected(self) -> None: def move_playlist_rows(self, session: Session,
playlistrows: List[PlaylistRows]) -> None:
""" """
Move selected rows to another playlist Move passed playlist rows to another playlist
Actions required: Actions required:
- identify destination playlist - identify destination playlist
@ -496,48 +497,74 @@ class Window(QMainWindow, Ui_MainWindow):
- update destination playlist display if loaded - update destination playlist display if loaded
""" """
if not playlistrows:
log.debug(f"musicmuster.move_playlist_rows({playlistrows=}")
# Identify destination playlist # Identify destination playlist
visible_tab = self.visible_playlist_tab()
source_playlist = visible_tab.playlist_id
playlists = []
for playlist in Playlists.get_all(session):
if playlist.id == source_playlist:
continue
else:
playlists.append(playlist)
# Get destination playlist id
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
dlg.exec()
if not dlg.playlist:
return
destination_playlist_id = dlg.playlist.id
# Remove moved rows from display
visible_tab.remove_rows([plr.row_number for plr in playlistrows])
# Update playlist for the rows in the database
last_row = PlaylistRows.get_last_used_row(session,
destination_playlist_id)
if last_row is not None:
next_row = last_row + 1
else:
next_row = 0
for plr in playlistrows:
plr.row_number = next_row
plr.playlist_id = destination_playlist_id
# Reset played as it's not been played on this playlist
plr.played = False
# Update destination playlist_tab if visible (if not visible, it
# will be re-populated when it is opened)
destionation_playlist_tab = None
for tab in range(self.tabPlaylist.count()):
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
destionation_playlist_tab = self.tabPlaylist.widget(tab)
break
if destionation_playlist_tab:
destionation_playlist_tab.populate(session, dlg.playlist.id)
def move_selected(self) -> None:
"""
Move selected rows to another playlist
"""
with Session() as session: with Session() as session:
visible_tab = self.visible_playlist_tab() self.move_playlist_rows(
source_playlist = visible_tab.playlist_id session,
playlists = [] self.visible_playlist_tab().get_selected_playlistrows(session)
for playlist in Playlists.get_all(session):
if playlist.id == source_playlist:
continue
else:
playlists.append(playlist)
# Get destination playlist id
dlg = SelectPlaylistDialog(self, playlists=playlists,
session=session)
dlg.exec()
if not dlg.playlist:
return
destination_playlist = dlg.playlist
# Update playlist for the rows in the database
plr_ids = visible_tab.get_selected_playlistrow_ids()
PlaylistRows.move_to_playlist(
session, plr_ids, destination_playlist.id
) )
# Remove moved rows from display def move_unplayed(self) -> None:
visible_tab.remove_selected_rows() """
Move unplayed rows to another playlist
"""
# Update destination playlist_tab if visible (if not visible, it playlist_id = self.visible_playlist_tab().playlist_id
# will be re-populated when it is opened) with Session() as session:
destination_visible_playlist_tab = None unplayed_playlist_rows = PlaylistRows.get_unplayed_rows(
for tab in range(self.tabPlaylist.count()): session, playlist_id)
# Non-playlist tabs won't have a 'playlist_id' attribute self.move_playlist_rows(session, unplayed_playlist_rows)
if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'):
continue
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
destination_visible_playlist_tab = (
self.tabPlaylist.widget(tab))
break
if destination_visible_playlist_tab:
destination_visible_playlist_tab.populate(
session, dlg.playlist.id)
def play_next(self) -> None: def play_next(self) -> None:
""" """
@ -638,12 +665,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.disable_play_next_controls() self.disable_play_next_controls()
self.txtSearch.setHidden(False) self.txtSearch.setHidden(False)
self.txtSearch.setFocus() self.txtSearch.setFocus()
# Select any text that may already be there
self.txtSearch.selectAll()
def search_playlist_clear(self) -> None: def search_playlist_clear(self) -> None:
"""Tidy up and reset search bar""" """Tidy up and reset search bar"""
# Clear the search text # Clear the search text
self.visible_playlist_tab().search("") self.visible_playlist_tab().set_search("")
# Clean up search bar # Clean up search bar
self.txtSearch.setText("") self.txtSearch.setText("")
self.txtSearch.setHidden(True) self.txtSearch.setHidden(True)
@ -651,7 +680,7 @@ class Window(QMainWindow, Ui_MainWindow):
def search_playlist_return(self) -> None: def search_playlist_return(self) -> None:
"""Initiate search when return pressed""" """Initiate search when return pressed"""
self.visible_playlist_tab().search(self.txtSearch.text()) self.visible_playlist_tab().set_search(self.txtSearch.text())
self.enable_play_next_controls() self.enable_play_next_controls()
# def search_playlist_update(self): # def search_playlist_update(self):
@ -674,21 +703,11 @@ class Window(QMainWindow, Ui_MainWindow):
"""Select next or first row in playlist""" """Select next or first row in playlist"""
self.visible_playlist_tab().select_next_row() self.visible_playlist_tab().select_next_row()
#
# def select_played(self) -> None:
# """Select all played tracks in playlist"""
#
# self.visible_playlist_tab().select_played_tracks()
def select_previous_row(self) -> None: def select_previous_row(self) -> None:
"""Select previous or first row in playlist""" """Select previous or first row in playlist"""
self.visible_playlist_tab().select_previous_row() self.visible_playlist_tab().select_previous_row()
#
# def select_unplayed(self) -> None:
# """Select all unplayed tracks in playlist"""
#
# self.visible_playlist_tab().select_unplayed_tracks()
def set_main_window_size(self) -> None: def set_main_window_size(self) -> None:
"""Set size of window from database""" """Set size of window from database"""

View File

@ -156,7 +156,7 @@ class PlaylistTab(QTableWidget):
self.populate(session, self.playlist_id) self.populate(session, self.playlist_id)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}" return f"<PlaylistTab(id={self.playlist_id}>"
# ########## Events other than cell editing ########## # ########## Events other than cell editing ##########
@ -472,7 +472,15 @@ class PlaylistTab(QTableWidget):
Return a list of PlaylistRow ids of the selected rows Return a list of PlaylistRow ids of the selected rows
""" """
return [self._get_playlistrow_id(a) for a in self._selected_rows()] return [self._get_playlistrow_id(a) for a in self._get_selected_rows()]
def get_selected_playlistrows(self, session: Session) -> Optional[List]:
"""
Return a list of PlaylistRows of the selected rows
"""
plr_ids = self.get_selected_playlistrow_ids()
return [session.get(PlaylistRows, a) for a in plr_ids]
def insert_header(self, session: Session, note: str, def insert_header(self, session: Session, note: str,
repaint: bool = True) -> None: repaint: bool = True) -> None:
@ -497,12 +505,6 @@ class PlaylistTab(QTableWidget):
if repaint: if repaint:
self.update_display(session) self.update_display(session)
# #
# def _get_selected_rows(self) -> List[int]:
# """Return a sorted list of selected row numbers"""
#
# rows = self.selectionModel().selectedRows()
# return sorted([row.row() for row in rows])
#
# def get_selected_title(self) -> Optional[str]: # def get_selected_title(self) -> Optional[str]:
# """Return title of selected row or None""" # """Return title of selected row or None"""
# #
@ -614,51 +616,6 @@ class PlaylistTab(QTableWidget):
PlaylistRows.fixup_rownumbers(session, self.playlist_id) PlaylistRows.fixup_rownumbers(session, self.playlist_id)
if repaint: if repaint:
self.update_display(session) self.update_display(session)
#
# def move_selected_to_playlist(self, session: Session, playlist_id: int) \
# -> None:
# """
# Move selected rows and any immediately preceding notes to
# other playlist
# """
#
# notes_rows = self._get_notes_rows()
# destination_row = Playlists.next_free_row(session, playlist_id)
# rows_to_remove = []
#
# for row in self._get_selected_rows():
# if row in notes_rows:
# note_obj = self._get_row_notes_object(row, session)
# note_obj.move_row(session, destination_row, playlist_id)
# else:
# # For tracks, check for a preceding notes row and move
# # that as well if it exists
# if row - 1 in notes_rows:
# note_obj = self._get_row_notes_object(row - 1, session)
# note_obj.move_row(session, destination_row, playlist_id)
# destination_row += 1
# rows_to_remove.append(row - 1)
# # Move track
# PlaylistTracks.move_row(
# session, row, self.playlist_id,
# destination_row, playlist_id
# )
# destination_row += 1
# rows_to_remove.append(row)
#
# # Remove rows. Row number will change as we delete rows so
# # remove them in reverse order.
#
# try:
# self.selecting_in_progress = True
# for row in sorted(rows_to_remove, reverse=True):
# self.removeRow(row)
# finally:
# self.selecting_in_progress = False
# self._select_event()
#
# self.save_playlist(session)
# self.update_display(session)
def play_started(self, session: Session) -> None: def play_started(self, session: Session) -> None:
""" """
@ -719,14 +676,6 @@ class PlaylistTab(QTableWidget):
Populate from the associated playlist ID Populate from the associated playlist ID
""" """
# data: List[Union[Tuple[List[int], Tracks], Tuple[List[int],
# Notes]]] \
# = []
# item: Union[Notes, Tracks]
# note: Notes
# row: int
# track: Tracks
# Sanity check row numbering before we load # Sanity check row numbering before we load
PlaylistRows.fixup_rownumbers(session, playlist_id) PlaylistRows.fixup_rownumbers(session, playlist_id)
@ -747,13 +696,18 @@ class PlaylistTab(QTableWidget):
# self.save_playlist(session) # self.save_playlist(session)
self.update_display(session) self.update_display(session)
def remove_selected_rows(self) -> None: def remove_rows(self, row_numbers: List[int]) -> None:
"""Remove selected rows from display""" """Remove passed rows from display"""
# Remove rows from display. Do so in reverse order so that # Remove rows from display. Do so in reverse order so that
# row numbers remain valid. # row numbers remain valid.
for row in sorted(self._selected_rows(), reverse=True): for row in sorted(row_numbers, reverse=True):
self.removeRow(row) self.removeRow(row)
def remove_selected_rows(self) -> None:
"""Remove selected rows from display"""
self.remove_rows(self._get_selected_rows())
# Reset drag mode # Reset drag mode
self.setDragEnabled(False) self.setDragEnabled(False)
@ -776,31 +730,37 @@ class PlaylistTab(QTableWidget):
session.commit() session.commit()
PlaylistRows.delete_higher_rows(session, self.playlist_id, row) PlaylistRows.delete_higher_rows(session, self.playlist_id, row)
def search(self, text: str) -> None: def set_search(self, text: str) -> None:
"""Set search text and find first match""" """Set search text and find first match"""
self.search_text = text self.search_text = text
if not text: if not text:
# Search string has been reset # Search string has been reset
return return
self.search_next() self._search(next=True)
def search_next(self) -> None: def _search(self, next: bool = True) -> None:
""" """
Select next row containg self.search_string. Start from Select next/previous row containg self.search_string. Start from
top selected row if there is one, else from top. top selected row if there is one, else from top.
Wrap at last row. Wrap at last/first row.
""" """
if not self.search_text: if not self.search_text:
return return
selected_row = self._get_selected_row() selected_row = self._get_selected_row()
if selected_row is not None and selected_row < self.rowCount() - 1: if next:
starting_row = selected_row + 1 if selected_row is not None and selected_row < self.rowCount() - 1:
starting_row = selected_row + 1
else:
starting_row = 0
else: else:
starting_row = 0 if selected_row is not None and selected_row > 0:
starting_row = selected_row - 1
else:
starting_row = self.rowCount() - 1
wrapped = False wrapped = False
match_row = None match_row = None
@ -820,60 +780,37 @@ class PlaylistTab(QTableWidget):
if note and needle in note.lower(): if note and needle in note.lower():
match_row = row match_row = row
break break
row += 1 if next:
if wrapped and row >= starting_row: row += 1
break if wrapped and row >= starting_row:
if row >= self.rowCount(): break
row = 0 if row >= self.rowCount():
wrapped = True row = 0
wrapped = True
else:
row -= 1
if wrapped and row <= starting_row:
break
if row < 0:
row = self.rowCount() - 1
wrapped = True
if match_row is not None: if match_row is not None:
self.selectRow(row) self.selectRow(row)
def search_next(self) -> None:
"""
Select next row containg self.search_string.
"""
self._search(next=True)
def search_previous(self) -> None: def search_previous(self) -> None:
""" """
Select previous row containg self.search_string. Start from Select previous row containg self.search_string.
top selected row if there is one, else from top.
Wrap at last row.
""" """
if not self.search_text: self._search(next=False)
return
selected_row = self._get_selected_row()
if selected_row is not None and selected_row > 0:
starting_row = selected_row - 1
else:
starting_row = self.rowCount() - 1
wrapped = False
match_row = None
row = starting_row
needle = self.search_text.lower()
while True:
# Check for match in title, artist or notes
title = self._get_row_title(row)
if title and needle in title.lower():
match_row = row
break
artist = self._get_row_title(row)
if artist and needle in artist.lower():
match_row = row
break
note = self._get_row_note(row)
if note and needle in note.lower():
match_row = row
break
row -= 1
if wrapped and row <= starting_row:
break
if row < 0:
row = self.rowCount() - 1
wrapped = True
if match_row is not None:
self.selectRow(row)
def select_next_row(self) -> None: def select_next_row(self) -> None:
""" """
@ -885,7 +822,7 @@ class PlaylistTab(QTableWidget):
row: int row: int
selected_rows: List[int] selected_rows: List[int]
selected_rows = self._selected_rows() selected_rows = self._get_selected_rows()
# we will only handle zero or one selected rows # we will only handle zero or one selected rows
if len(selected_rows) > 1: if len(selected_rows) > 1:
return return
@ -912,16 +849,6 @@ class PlaylistTab(QTableWidget):
track_id = self._get_row_track_id(row) track_id = self._get_row_track_id(row)
self.selectRow(row) self.selectRow(row)
#
# def select_played_tracks(self) -> None:
# """Select all played tracks in playlist"""
#
# try:
# self.selecting_in_progress = True
# self._select_tracks(played=True)
# finally:
# self.selecting_in_progress = False
# self._select_event()
def select_previous_row(self) -> None: def select_previous_row(self) -> None:
""" """
@ -932,7 +859,7 @@ class PlaylistTab(QTableWidget):
row: int row: int
selected_rows: List[int] selected_rows: List[int]
selected_rows = self._selected_rows() selected_rows = self._get_selected_rows()
# we will only handle zero or one selected rows # we will only handle zero or one selected rows
if len(selected_rows) > 1: if len(selected_rows) > 1:
return return
@ -960,16 +887,6 @@ class PlaylistTab(QTableWidget):
track_id = self._get_row_track_id(row) track_id = self._get_row_track_id(row)
self.selectRow(row) self.selectRow(row)
#
# def select_unplayed_tracks(self) -> None:
# """Select all unplayed tracks in playlist"""
#
# try:
# self.selecting_in_progress = True
# self._select_tracks(played=False)
# finally:
# self.selecting_in_progress = False
# self._select_event()
def set_searchtext(self, text: Optional[str]) -> None: def set_searchtext(self, text: Optional[str]) -> None:
"""Set the search text and find first match""" """Set the search text and find first match"""
@ -1410,14 +1327,6 @@ class PlaylistTab(QTableWidget):
return track_id return track_id
# def _get_unplayed_track_rows(self) -> Optional[List[int]]:
# """Return rows marked as unplayed, or None"""
#
# unplayed_rows: Set[int] = set(self._meta_notset(RowMeta.PLAYED))
# notes_rows: Set[int] = set(self._get_notes_rows())
#
# return list(unplayed_rows - notes_rows)
def _get_selected_row(self) -> Optional[int]: def _get_selected_row(self) -> Optional[int]:
"""Return row number of first selected row, or None if none selected""" """Return row number of first selected row, or None if none selected"""
@ -1426,6 +1335,13 @@ class PlaylistTab(QTableWidget):
else: else:
return self.selectionModel().selectedRows()[0].row() return self.selectionModel().selectedRows()[0].row()
def _get_selected_rows(self) -> List[int]:
"""Return a list of selected row numbers"""
# Use a set to deduplicate result (a selected row will have all
# items in that row selected)
return [row for row in set([a.row() for a in self.selectedItems()])]
def _get_unreadable_track_rows(self) -> List[int]: def _get_unreadable_track_rows(self) -> List[int]:
"""Return rows marked as unreadable, or None""" """Return rows marked as unreadable, or None"""
@ -1456,48 +1372,6 @@ class PlaylistTab(QTableWidget):
info.setStandardButtons(QMessageBox.Ok) info.setStandardButtons(QMessageBox.Ok)
info.setDefaultButton(QMessageBox.Cancel) info.setDefaultButton(QMessageBox.Cancel)
info.exec() info.exec()
#
# def _insert_note(self, session: Session, note: Notes,
# row: Optional[int] = None, repaint: bool = True) ->
# None:
# """
# Insert a note to playlist tab.
#
# If a row is given, add note above. Otherwise, add to end of
# playlist.
# """
#
# if row is None:
# row = self.rowCount()
# log.debug(f"playlist.inset_note(): row={row}")
#
# self.insertRow(row)
#
# # Add empty items to unused columns because
# # colour won't be set for columns without items
# item: QTableWidgetItem = QTableWidgetItem()
# self.setItem(row, FIXUP.COL_USERDATA, item)
# item = QTableWidgetItem()
# self.setItem(row, FIXUP.COL_MSS, item)
#
# # Add text of note from title column onwards
# titleitem: QTableWidgetItem = QTableWidgetItem(note.note)
# self.setItem(row, FIXUP.COL_NOTE, titleitem)
# self.setSpan(row, FIXUP.COL_NOTE, self.NOTE_ROW_SPAN,
# self.NOTE_COL_SPAN)
#
# # Attach note id to row
# self._set_row_content(row, note.id)
#
# # Mark row as a Note row
# self._set_note_row(row)
#
# # Scroll to new row
# self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
#
# if repaint:
# self.save_playlist(session)
# self.update_display(session, clear_selection=False)
def _is_below(self, pos, index): # review def _is_below(self, pos, index): # review
""" """
@ -1516,16 +1390,6 @@ class PlaylistTab(QTableWidget):
and pos.y() >= rect.center().y() # noqa W503 and pos.y() >= rect.center().y() # noqa W503
) )
# def _is_note_row(self, row: int) -> bool:
# """
# Return True if passed row is a note row, else False
# """
#
# if self._meta_get(row):
# if self._meta_get(row) & (1 << RowMeta.NOTE):
# return True
# return False
def _meta_clear_attribute(self, row: int, attribute: int) -> None: def _meta_clear_attribute(self, row: int, attribute: int) -> None:
"""Clear given metadata for row""" """Clear given metadata for row"""
@ -1550,22 +1414,6 @@ class PlaylistTab(QTableWidget):
return (self.item(row, columns['userdata'].idx) return (self.item(row, columns['userdata'].idx)
.data(self.ROW_FLAGS)) .data(self.ROW_FLAGS))
#
# def _meta_notset(self, metadata: int) -> List[int]:
# """
# Search rows for metadata not set.
#
# Return a list of matching row numbers.
# """
#
# matches = []
# for row in range(self.rowCount()):
# row_meta = self._meta_get(row)
# if row_meta is not None:
# if not self._meta_get(row) & (1 << metadata):
# matches.append(row)
#
# return matches
def _meta_search(self, metadata: int, one: bool = True) -> List[int]: def _meta_search(self, metadata: int, one: bool = True) -> List[int]:
""" """
@ -1697,7 +1545,7 @@ class PlaylistTab(QTableWidget):
if self.selecting_in_progress: if self.selecting_in_progress:
return return
selected_rows = self._selected_rows() selected_rows = self._get_selected_rows()
# If no rows are selected, we have nothing to do # If no rows are selected, we have nothing to do
if len(selected_rows) == 0: if len(selected_rows) == 0:
self.musicmuster.lblSumPlaytime.setText("") self.musicmuster.lblSumPlaytime.setText("")
@ -1714,13 +1562,6 @@ class PlaylistTab(QTableWidget):
else: else:
self.musicmuster.lblSumPlaytime.setText("") self.musicmuster.lblSumPlaytime.setText("")
def _selected_rows(self) -> List[int]:
"""Return a list of selected row numbers"""
# Use a set to deduplicate result (a selected row will have all
# items in that row selected)
return [row for row in set([a.row() for a in self.selectedItems()])]
def _set_column_widths(self, session: Session) -> None: def _set_column_widths(self, session: Session) -> None:
"""Column widths from settings""" """Column widths from settings"""
@ -1817,14 +1658,6 @@ class PlaylistTab(QTableWidget):
for j in range(1, self.columnCount()): for j in range(1, self.columnCount()):
if self.item(row, j): if self.item(row, j):
self.item(row, j).setBackground(brush) self.item(row, j).setBackground(brush)
#
# def _set_row_content(self, row: int, object_id: int) -> None:
# """Set content associated with this row"""
#
# assert self.item(row, FIXUP.COL_USERDATA)
#
# self.item(row, FIXUP.COL_USERDATA).setData(
# self.CONTENT_OBJECT, object_id)
def _set_row_duration(self, row: int, ms: int) -> None: def _set_row_duration(self, row: int, ms: int) -> None:
"""Set duration of this row in row metadata""" """Set duration of this row in row metadata"""
@ -1860,27 +1693,6 @@ class PlaylistTab(QTableWidget):
"""Mark this row as unreadable""" """Mark this row as unreadable"""
self._meta_set_attribute(row, RowMeta.UNREADABLE) self._meta_set_attribute(row, RowMeta.UNREADABLE)
#
# 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(QAbstractItemView.MultiSelection)
# self.clear_selection()
#
# if played:
# rows = self._get_played_track_rows()
# else:
# rows = self._get_unplayed_track_rows()
#
# for row in rows:
# self.selectRow(row)
#
# # Reset extended selection
# self.setSelectionMode(QAbstractItemView.ExtendedSelection)
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:

View File

@ -763,7 +763,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<addaction name="actionDeletePlaylist"/> <addaction name="actionDeletePlaylist"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionMoveSelected"/> <addaction name="actionMoveSelected"/>
<addaction name="actionMove_unplayed"/> <addaction name="actionMoveUnplayed"/>
<addaction name="actionDownload_CSV_of_played_tracks"/> <addaction name="actionDownload_CSV_of_played_tracks"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionE_xit"/> <addaction name="actionE_xit"/>
@ -1000,7 +1000,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>Select played tracks</string> <string>Select played tracks</string>
</property> </property>
</action> </action>
<action name="actionMove_unplayed"> <action name="actionMoveUnplayed">
<property name="text"> <property name="text">
<string>Move &amp;unplayed tracks to...</string> <string>Move &amp;unplayed tracks to...</string>
</property> </property>

View File

@ -430,8 +430,8 @@ class Ui_MainWindow(object):
self.actionSelect_previous_track.setObjectName("actionSelect_previous_track") self.actionSelect_previous_track.setObjectName("actionSelect_previous_track")
self.actionSelect_played_tracks = QtWidgets.QAction(MainWindow) self.actionSelect_played_tracks = QtWidgets.QAction(MainWindow)
self.actionSelect_played_tracks.setObjectName("actionSelect_played_tracks") self.actionSelect_played_tracks.setObjectName("actionSelect_played_tracks")
self.actionMove_unplayed = QtWidgets.QAction(MainWindow) self.actionMoveUnplayed = QtWidgets.QAction(MainWindow)
self.actionMove_unplayed.setObjectName("actionMove_unplayed") self.actionMoveUnplayed.setObjectName("actionMoveUnplayed")
self.actionAdd_note = QtWidgets.QAction(MainWindow) self.actionAdd_note = QtWidgets.QAction(MainWindow)
self.actionAdd_note.setObjectName("actionAdd_note") self.actionAdd_note.setObjectName("actionAdd_note")
self.actionEnable_controls = QtWidgets.QAction(MainWindow) self.actionEnable_controls = QtWidgets.QAction(MainWindow)
@ -458,7 +458,7 @@ class Ui_MainWindow(object):
self.menuFile.addAction(self.actionDeletePlaylist) self.menuFile.addAction(self.actionDeletePlaylist)
self.menuFile.addSeparator() self.menuFile.addSeparator()
self.menuFile.addAction(self.actionMoveSelected) self.menuFile.addAction(self.actionMoveSelected)
self.menuFile.addAction(self.actionMove_unplayed) self.menuFile.addAction(self.actionMoveUnplayed)
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks) self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
self.menuFile.addSeparator() self.menuFile.addSeparator()
self.menuFile.addAction(self.actionE_xit) self.menuFile.addAction(self.actionE_xit)
@ -559,7 +559,7 @@ class Ui_MainWindow(object):
self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track")) self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track"))
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K")) self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks")) self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
self.actionMove_unplayed.setText(_translate("MainWindow", "Move &unplayed tracks to...")) self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
self.actionAdd_note.setText(_translate("MainWindow", "Add note...")) self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T")) self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls")) self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))