Compare commits
No commits in common. "3b4cf5320daed2324ac74cb018e2e50d9063e591" and "8fedb394a4e84bae24aeefb916f2e5f84b47dca9" have entirely different histories.
3b4cf5320d
...
8fedb394a4
@ -24,8 +24,7 @@ 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):
|
||||||
# Exceptions are logged at ERROR level
|
if record.levelno != logging.DEBUG:
|
||||||
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
|
||||||
|
|||||||
570
app/models.py
570
app/models.py
@ -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,6 +127,91 @@ 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'
|
||||||
|
|
||||||
@ -178,16 +263,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):
|
||||||
@ -215,19 +300,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"""
|
||||||
@ -282,20 +367,66 @@ 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):
|
||||||
@ -425,33 +556,68 @@ class PlaylistRows(Base):
|
|||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_last_used_row(session: Session, playlist_id: int) -> Optional[int]:
|
def move_to_playlist(session: Session,
|
||||||
"""Return the last used row for playlist, or None if no rows"""
|
playlistrow_ids: List[int],
|
||||||
|
destination_playlist_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Move the list of playlistrow_ids to the end of destination_playlist
|
||||||
|
"""
|
||||||
|
|
||||||
return session.execute(
|
# Find last row of destination playlist
|
||||||
|
last_row = session.execute(
|
||||||
select(func.max(PlaylistRows.row_number))
|
select(func.max(PlaylistRows.row_number))
|
||||||
.where(PlaylistRows.playlist_id == playlist_id)
|
.where(PlaylistRows.playlist_id == destination_playlist_id)
|
||||||
).scalar_one()
|
).scalar_one()
|
||||||
|
if last_row is None:
|
||||||
|
last_row = 0
|
||||||
|
|
||||||
@classmethod
|
# Update the PlaylistRows entries
|
||||||
def get_unplayed_rows(cls, session: Session,
|
for plr_id in playlistrow_ids:
|
||||||
playlist_id: int) -> List[int]:
|
last_row += 1
|
||||||
"""
|
plr = session.get(PlaylistRows, plr_id)
|
||||||
For passed playlist, return a list of track rows that
|
plr.row_number = last_row
|
||||||
have not been played.
|
plr.playlist_id = destination_playlist_id
|
||||||
"""
|
|
||||||
|
|
||||||
plrs = session.execute(
|
session.commit()
|
||||||
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()
|
|
||||||
|
|
||||||
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):
|
||||||
@ -518,99 +684,100 @@ 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__(
|
#
|
||||||
# self,
|
# def __init__(
|
||||||
# session: Session,
|
# self,
|
||||||
# path: str,
|
# session: Session,
|
||||||
# title: Optional[str] = None,
|
# path: str,
|
||||||
# artist: Optional[str] = None,
|
# title: Optional[str] = None,
|
||||||
# duration: int = 0,
|
# artist: Optional[str] = None,
|
||||||
# start_gap: int = 0,
|
# duration: int = 0,
|
||||||
# fade_at: Optional[int] = None,
|
# start_gap: int = 0,
|
||||||
# silence_at: Optional[int] = None,
|
# fade_at: Optional[int] = None,
|
||||||
# mtime: Optional[float] = None,
|
# silence_at: Optional[int] = None,
|
||||||
# lastplayed: Optional[datetime] = None,
|
# mtime: Optional[float] = None,
|
||||||
# ) -> None:
|
# lastplayed: Optional[datetime] = None,
|
||||||
# self.path = path
|
# ) -> None:
|
||||||
# self.title = title
|
# self.path = path
|
||||||
# self.artist = artist
|
# self.title = title
|
||||||
# self.duration = duration
|
# self.artist = artist
|
||||||
# self.start_gap = start_gap
|
# self.duration = duration
|
||||||
# self.fade_at = fade_at
|
# self.start_gap = start_gap
|
||||||
# self.silence_at = silence_at
|
# self.fade_at = fade_at
|
||||||
# self.mtime = mtime
|
# self.silence_at = silence_at
|
||||||
# self.lastplayed = lastplayed
|
# self.mtime = mtime
|
||||||
#
|
# self.lastplayed = lastplayed
|
||||||
# session.add(self)
|
#
|
||||||
# session.flush()
|
# session.add(self)
|
||||||
#
|
# session.flush()
|
||||||
# @staticmethod
|
#
|
||||||
# def get_all_paths(session) -> List[str]:
|
# @staticmethod
|
||||||
# """Return a list of paths of all tracks"""
|
# def get_all_paths(session) -> List[str]:
|
||||||
#
|
# """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
|
#
|
||||||
# def get_all_tracks(cls, session: Session) -> List["Tracks"]:
|
# @classmethod
|
||||||
# """Return a list of all tracks"""
|
# def get_all_tracks(cls, session: Session) -> List["Tracks"]:
|
||||||
#
|
# """Return a list of all tracks"""
|
||||||
# return session.query(cls).all()
|
#
|
||||||
#
|
# return session.query(cls).all()
|
||||||
# @classmethod
|
#
|
||||||
# def get_or_create(cls, session: Session, path: str) -> "Tracks":
|
# @classmethod
|
||||||
# """
|
# def get_or_create(cls, session: Session, path: str) -> "Tracks":
|
||||||
# If a track with path exists, return it;
|
# """
|
||||||
# else created new track and return it
|
# If a track with path exists, 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:
|
#
|
||||||
# track = session.query(cls).filter(cls.path == path).one()
|
# try:
|
||||||
# except NoResultFound:
|
# track = session.query(cls).filter(cls.path == path).one()
|
||||||
# track = Tracks(session, path)
|
# except NoResultFound:
|
||||||
#
|
# track = Tracks(session, path)
|
||||||
# return track
|
#
|
||||||
#
|
# return track
|
||||||
# @classmethod
|
#
|
||||||
# def get_by_filename(cls, session: Session, filename: str) \
|
# @classmethod
|
||||||
# -> Optional["Tracks"]:
|
# def get_by_filename(cls, session: Session, filename: str) \
|
||||||
# """
|
# -> Optional["Tracks"]:
|
||||||
# Return track if one and only one track in database has passed
|
# """
|
||||||
# filename (ie, basename of path). Return None if zero or more
|
# Return track if one and only one track in database has passed
|
||||||
# than one track matches.
|
# filename (ie, basename of path). Return None if zero or more
|
||||||
# """
|
# than one track matches.
|
||||||
#
|
# """
|
||||||
# log.debug(f"Tracks.get_track_from_filename({filename=})")
|
#
|
||||||
# try:
|
# log.debug(f"Tracks.get_track_from_filename({filename=})")
|
||||||
# track = session.query(Tracks).filter(Tracks.path.ilike(
|
# try:
|
||||||
# f'%{os.path.sep}{filename}')).one()
|
# track = session.query(Tracks).filter(Tracks.path.ilike(
|
||||||
# return track
|
# f'%{os.path.sep}{filename}')).one()
|
||||||
# except (NoResultFound, MultipleResultsFound):
|
# return track
|
||||||
# return None
|
# except (NoResultFound, MultipleResultsFound):
|
||||||
#
|
# return None
|
||||||
# @classmethod
|
#
|
||||||
# def get_by_path(cls, session: Session, path: str) -> List["Tracks"]:
|
# @classmethod
|
||||||
# """
|
# 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
|
#
|
||||||
# def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]:
|
# @classmethod
|
||||||
# """Return track or None"""
|
# def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]:
|
||||||
#
|
# """Return track or None"""
|
||||||
# try:
|
#
|
||||||
# log.debug(f"Tracks.get_track(track_id={track_id})")
|
# try:
|
||||||
# track = session.query(Tracks).filter(Tracks.id == track_id).one()
|
# log.debug(f"Tracks.get_track(track_id={track_id})")
|
||||||
# return track
|
# track = session.query(Tracks).filter(Tracks.id == track_id).one()
|
||||||
# except NoResultFound:
|
# return track
|
||||||
# log.error(f"get_track({track_id}): not found")
|
# except NoResultFound:
|
||||||
# return None
|
# log.error(f"get_track({track_id}): not found")
|
||||||
|
# return None
|
||||||
|
|
||||||
def rescan(self, session: Session) -> None:
|
def rescan(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
@ -627,18 +794,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"]:
|
||||||
@ -666,3 +833,26 @@ 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()
|
||||||
|
|||||||
@ -7,7 +7,6 @@ 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
|
||||||
@ -167,7 +166,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)
|
||||||
@ -176,7 +175,8 @@ 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.actionMoveUnplayed.triggered.connect(self.move_unplayed)
|
# self.actionSelect_unplayed_tracks.triggered.connect(
|
||||||
|
# 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,10 +485,9 @@ 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_playlist_rows(self, session: Session,
|
def move_selected(self) -> None:
|
||||||
playlistrows: List[PlaylistRows]) -> None:
|
|
||||||
"""
|
"""
|
||||||
Move passed playlist rows to another playlist
|
Move selected rows to another playlist
|
||||||
|
|
||||||
Actions required:
|
Actions required:
|
||||||
- identify destination playlist
|
- identify destination playlist
|
||||||
@ -497,10 +496,8 @@ 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
|
||||||
|
with Session() as session:
|
||||||
visible_tab = self.visible_playlist_tab()
|
visible_tab = self.visible_playlist_tab()
|
||||||
source_playlist = visible_tab.playlist_id
|
source_playlist = visible_tab.playlist_id
|
||||||
playlists = []
|
playlists = []
|
||||||
@ -511,60 +508,36 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
playlists.append(playlist)
|
playlists.append(playlist)
|
||||||
|
|
||||||
# Get destination playlist id
|
# Get destination playlist id
|
||||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
dlg = SelectPlaylistDialog(self, playlists=playlists,
|
||||||
|
session=session)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
if not dlg.playlist:
|
if not dlg.playlist:
|
||||||
return
|
return
|
||||||
destination_playlist_id = dlg.playlist.id
|
destination_playlist = dlg.playlist
|
||||||
|
|
||||||
# Remove moved rows from display
|
|
||||||
visible_tab.remove_rows([plr.row_number for plr in playlistrows])
|
|
||||||
|
|
||||||
# Update playlist for the rows in the database
|
# Update playlist for the rows in the database
|
||||||
last_row = PlaylistRows.get_last_used_row(session,
|
plr_ids = visible_tab.get_selected_playlistrow_ids()
|
||||||
destination_playlist_id)
|
PlaylistRows.move_to_playlist(
|
||||||
if last_row is not None:
|
session, plr_ids, destination_playlist.id
|
||||||
next_row = last_row + 1
|
)
|
||||||
else:
|
|
||||||
next_row = 0
|
|
||||||
|
|
||||||
for plr in playlistrows:
|
# Remove moved rows from display
|
||||||
plr.row_number = next_row
|
visible_tab.remove_selected_rows()
|
||||||
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
|
# Update destination playlist_tab if visible (if not visible, it
|
||||||
# will be re-populated when it is opened)
|
# will be re-populated when it is opened)
|
||||||
destionation_playlist_tab = None
|
destination_visible_playlist_tab = None
|
||||||
for tab in range(self.tabPlaylist.count()):
|
for tab in range(self.tabPlaylist.count()):
|
||||||
|
# Non-playlist tabs won't have a 'playlist_id' attribute
|
||||||
|
if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'):
|
||||||
|
continue
|
||||||
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
|
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
|
||||||
destionation_playlist_tab = self.tabPlaylist.widget(tab)
|
destination_visible_playlist_tab = (
|
||||||
|
self.tabPlaylist.widget(tab))
|
||||||
break
|
break
|
||||||
if destionation_playlist_tab:
|
if destination_visible_playlist_tab:
|
||||||
destionation_playlist_tab.populate(session, dlg.playlist.id)
|
destination_visible_playlist_tab.populate(
|
||||||
|
session, dlg.playlist.id)
|
||||||
def move_selected(self) -> None:
|
|
||||||
"""
|
|
||||||
Move selected rows to another playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
with Session() as session:
|
|
||||||
self.move_playlist_rows(
|
|
||||||
session,
|
|
||||||
self.visible_playlist_tab().get_selected_playlistrows(session)
|
|
||||||
)
|
|
||||||
|
|
||||||
def move_unplayed(self) -> None:
|
|
||||||
"""
|
|
||||||
Move unplayed rows to another playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
playlist_id = self.visible_playlist_tab().playlist_id
|
|
||||||
with Session() as session:
|
|
||||||
unplayed_playlist_rows = PlaylistRows.get_unplayed_rows(
|
|
||||||
session, playlist_id)
|
|
||||||
self.move_playlist_rows(session, unplayed_playlist_rows)
|
|
||||||
|
|
||||||
def play_next(self) -> None:
|
def play_next(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -665,14 +638,12 @@ 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().set_search("")
|
self.visible_playlist_tab().search("")
|
||||||
# Clean up search bar
|
# Clean up search bar
|
||||||
self.txtSearch.setText("")
|
self.txtSearch.setText("")
|
||||||
self.txtSearch.setHidden(True)
|
self.txtSearch.setHidden(True)
|
||||||
@ -680,7 +651,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().set_search(self.txtSearch.text())
|
self.visible_playlist_tab().search(self.txtSearch.text())
|
||||||
self.enable_play_next_controls()
|
self.enable_play_next_controls()
|
||||||
|
|
||||||
# def search_playlist_update(self):
|
# def search_playlist_update(self):
|
||||||
@ -703,11 +674,21 @@ 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"""
|
||||||
|
|||||||
298
app/playlists.py
298
app/playlists.py
@ -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,15 +472,7 @@ 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._get_selected_rows()]
|
return [self._get_playlistrow_id(a) for a in self._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:
|
||||||
@ -505,6 +497,12 @@ 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"""
|
||||||
#
|
#
|
||||||
@ -616,6 +614,51 @@ 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:
|
||||||
"""
|
"""
|
||||||
@ -676,6 +719,14 @@ 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)
|
||||||
|
|
||||||
@ -696,18 +747,13 @@ class PlaylistTab(QTableWidget):
|
|||||||
# self.save_playlist(session)
|
# self.save_playlist(session)
|
||||||
self.update_display(session)
|
self.update_display(session)
|
||||||
|
|
||||||
def remove_rows(self, row_numbers: List[int]) -> None:
|
|
||||||
"""Remove passed rows from display"""
|
|
||||||
|
|
||||||
# Remove rows from display. Do so in reverse order so that
|
|
||||||
# row numbers remain valid.
|
|
||||||
for row in sorted(row_numbers, reverse=True):
|
|
||||||
self.removeRow(row)
|
|
||||||
|
|
||||||
def remove_selected_rows(self) -> None:
|
def remove_selected_rows(self) -> None:
|
||||||
"""Remove selected rows from display"""
|
"""Remove selected rows from display"""
|
||||||
|
|
||||||
self.remove_rows(self._get_selected_rows())
|
# Remove rows from display. Do so in reverse order so that
|
||||||
|
# row numbers remain valid.
|
||||||
|
for row in sorted(self._selected_rows(), reverse=True):
|
||||||
|
self.removeRow(row)
|
||||||
# Reset drag mode
|
# Reset drag mode
|
||||||
self.setDragEnabled(False)
|
self.setDragEnabled(False)
|
||||||
|
|
||||||
@ -730,37 +776,31 @@ 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 set_search(self, text: str) -> None:
|
def 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=True)
|
self.search_next()
|
||||||
|
|
||||||
def _search(self, next: bool = True) -> None:
|
def search_next(self) -> None:
|
||||||
"""
|
"""
|
||||||
Select next/previous row containg self.search_string. Start from
|
Select next 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/first row.
|
Wrap at last 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 next:
|
|
||||||
if selected_row is not None and selected_row < self.rowCount() - 1:
|
if selected_row is not None and selected_row < self.rowCount() - 1:
|
||||||
starting_row = selected_row + 1
|
starting_row = selected_row + 1
|
||||||
else:
|
else:
|
||||||
starting_row = 0
|
starting_row = 0
|
||||||
else:
|
|
||||||
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
|
||||||
@ -780,14 +820,51 @@ 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
|
||||||
if next:
|
|
||||||
row += 1
|
row += 1
|
||||||
if wrapped and row >= starting_row:
|
if wrapped and row >= starting_row:
|
||||||
break
|
break
|
||||||
if row >= self.rowCount():
|
if row >= self.rowCount():
|
||||||
row = 0
|
row = 0
|
||||||
wrapped = True
|
wrapped = True
|
||||||
|
|
||||||
|
if match_row is not None:
|
||||||
|
self.selectRow(row)
|
||||||
|
|
||||||
|
def search_previous(self) -> None:
|
||||||
|
"""
|
||||||
|
Select previous row containg self.search_string. Start from
|
||||||
|
top selected row if there is one, else from top.
|
||||||
|
|
||||||
|
Wrap at last row.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.search_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
selected_row = self._get_selected_row()
|
||||||
|
if selected_row is not None and selected_row > 0:
|
||||||
|
starting_row = selected_row - 1
|
||||||
else:
|
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
|
row -= 1
|
||||||
if wrapped and row <= starting_row:
|
if wrapped and row <= starting_row:
|
||||||
break
|
break
|
||||||
@ -798,20 +875,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
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:
|
|
||||||
"""
|
|
||||||
Select previous row containg self.search_string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._search(next=False)
|
|
||||||
|
|
||||||
def select_next_row(self) -> None:
|
def select_next_row(self) -> None:
|
||||||
"""
|
"""
|
||||||
Select next or first row. Don't select section headers.
|
Select next or first row. Don't select section headers.
|
||||||
@ -822,7 +885,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
row: int
|
row: int
|
||||||
selected_rows: List[int]
|
selected_rows: List[int]
|
||||||
|
|
||||||
selected_rows = self._get_selected_rows()
|
selected_rows = self._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
|
||||||
@ -849,6 +912,16 @@ 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:
|
||||||
"""
|
"""
|
||||||
@ -859,7 +932,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
row: int
|
row: int
|
||||||
selected_rows: List[int]
|
selected_rows: List[int]
|
||||||
|
|
||||||
selected_rows = self._get_selected_rows()
|
selected_rows = self._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
|
||||||
@ -887,6 +960,16 @@ 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"""
|
||||||
@ -1327,6 +1410,14 @@ 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"""
|
||||||
|
|
||||||
@ -1335,13 +1426,6 @@ 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"""
|
||||||
|
|
||||||
@ -1372,6 +1456,48 @@ 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
|
||||||
"""
|
"""
|
||||||
@ -1390,6 +1516,16 @@ 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"""
|
||||||
|
|
||||||
@ -1414,6 +1550,22 @@ 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]:
|
||||||
"""
|
"""
|
||||||
@ -1545,7 +1697,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
if self.selecting_in_progress:
|
if self.selecting_in_progress:
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_rows = self._get_selected_rows()
|
selected_rows = self._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("")
|
||||||
@ -1562,6 +1714,13 @@ 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"""
|
||||||
|
|
||||||
@ -1658,6 +1817,14 @@ 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"""
|
||||||
@ -1693,6 +1860,27 @@ 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:
|
||||||
|
|||||||
@ -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="actionMoveUnplayed"/>
|
<addaction name="actionMove_unplayed"/>
|
||||||
<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="actionMoveUnplayed">
|
<action name="actionMove_unplayed">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Move &unplayed tracks to...</string>
|
<string>Move &unplayed tracks to...</string>
|
||||||
</property>
|
</property>
|
||||||
|
|||||||
@ -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.actionMoveUnplayed = QtWidgets.QAction(MainWindow)
|
self.actionMove_unplayed = QtWidgets.QAction(MainWindow)
|
||||||
self.actionMoveUnplayed.setObjectName("actionMoveUnplayed")
|
self.actionMove_unplayed.setObjectName("actionMove_unplayed")
|
||||||
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.actionMoveUnplayed)
|
self.menuFile.addAction(self.actionMove_unplayed)
|
||||||
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.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
|
self.actionMove_unplayed.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"))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user