Remove unused code

This commit is contained in:
Keith Edmunds 2022-08-15 12:45:45 +01:00
parent d5950ab29a
commit 3b4cf5320d
3 changed files with 170 additions and 529 deletions

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()
# 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() # session.flush()
# #
# @staticmethod
# def next_free_row(session: Session, playlist_id: int) -> int:
# """Return next free row for this playlist"""
#
# max_notes_row = Notes.max_used_row(session, playlist_id)
# max_tracks_row = xPlaylistTracks.max_used_row(session, playlist_id)
#
# if max_notes_row is not None and max_tracks_row is not None:
# return max(max_notes_row, max_tracks_row) + 1
#
# if max_notes_row is None and max_tracks_row is None:
# 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):
@ -584,45 +453,6 @@ class PlaylistRows(Base):
return plrs return plrs
# @classmethod
# 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):
"""Manage settings""" """Manage settings"""
@ -688,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:
""" """
@ -798,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"]:
@ -837,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

@ -703,11 +703,6 @@ 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"""

View File

@ -616,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:
""" """
@ -721,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)
@ -902,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:
""" """
@ -1435,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
""" """
@ -1495,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"""
@ -1529,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]:
""" """
@ -1789,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"""
@ -1832,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: