Compare commits
8 Commits
eae8870d4d
...
262ab202fc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
262ab202fc | ||
|
|
4f4408400f | ||
|
|
f4a374f68c | ||
|
|
77774dc403 | ||
|
|
8f2ab98be0 | ||
|
|
199f0e27fa | ||
|
|
e37f62fe87 | ||
|
|
be7071aae0 |
@ -90,7 +90,7 @@ class Config(object):
|
||||
ROWS_FROM_ZERO = True
|
||||
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
|
||||
SCROLL_TOP_MARGIN = 3
|
||||
START_GAP_WARNING_THRESHOLD = 500
|
||||
START_GAP_WARNING_THRESHOLD = 300
|
||||
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
|
||||
TOD_TIME_FORMAT = "%H:%M:%S"
|
||||
TRACK_TIME_FORMAT = "%H:%M:%S"
|
||||
|
||||
@ -25,7 +25,6 @@ from sqlalchemy import (
|
||||
from sqlalchemy.orm import (
|
||||
DeclarativeBase,
|
||||
joinedload,
|
||||
lazyload,
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
@ -217,7 +216,8 @@ class Playlists(Base):
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(32), unique=True)
|
||||
last_used: Mapped[Optional[datetime]] = mapped_column(DateTime, default=None)
|
||||
tab: Mapped[Optional[int]] = mapped_column(default=None, unique=True)
|
||||
tab: Mapped[Optional[int]] = mapped_column(default=None)
|
||||
open: Mapped[bool] = mapped_column(default=False)
|
||||
is_template: Mapped[bool] = mapped_column(default=False)
|
||||
deleted: Mapped[bool] = mapped_column(default=False)
|
||||
rows: Mapped[List["PlaylistRows"]] = relationship(
|
||||
@ -230,7 +230,7 @@ class Playlists(Base):
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Playlists(id={self.id}, name={self.name}, "
|
||||
f"is_templatee={self.is_template}>"
|
||||
f"is_templatee={self.is_template}, open={self.open}>"
|
||||
)
|
||||
|
||||
def __init__(self, session: scoped_session, name: str):
|
||||
@ -238,19 +238,10 @@ class Playlists(Base):
|
||||
session.add(self)
|
||||
session.flush()
|
||||
|
||||
def close(self, session: scoped_session) -> None:
|
||||
def close(self) -> None:
|
||||
"""Mark playlist as unloaded"""
|
||||
|
||||
closed_idx = self.tab
|
||||
self.tab = None
|
||||
|
||||
# Closing this tab will mean all higher-number tabs have moved
|
||||
# down by one
|
||||
session.execute(
|
||||
update(Playlists)
|
||||
.where(Playlists.tab > closed_idx)
|
||||
.values(tab=Playlists.tab - 1)
|
||||
)
|
||||
self.open = False
|
||||
|
||||
@classmethod
|
||||
def create_playlist_from_template(
|
||||
@ -283,7 +274,7 @@ class Playlists(Base):
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.filter(cls.is_template.is_(False))
|
||||
.order_by(cls.tab.desc(), cls.last_used.desc())
|
||||
.order_by(cls.last_used.desc())
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
@ -301,7 +292,7 @@ class Playlists(Base):
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.filter(
|
||||
cls.tab.is_(None),
|
||||
cls.open.is_(False),
|
||||
cls.is_template.is_(False),
|
||||
cls.deleted.is_(False),
|
||||
)
|
||||
@ -311,32 +302,29 @@ class Playlists(Base):
|
||||
@classmethod
|
||||
def get_open(cls, session: scoped_session) -> Sequence[Optional["Playlists"]]:
|
||||
"""
|
||||
Return a list of loaded playlists ordered by tab order.
|
||||
Return a list of loaded playlists ordered by tab.
|
||||
"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls).where(cls.tab.is_not(None)).order_by(cls.tab)
|
||||
select(cls).where(cls.open.is_(True))
|
||||
.order_by(cls.tab)
|
||||
).all()
|
||||
|
||||
def mark_open(self, session: scoped_session, tab_index: int) -> None:
|
||||
def mark_open(self) -> None:
|
||||
"""Mark playlist as loaded and used now"""
|
||||
|
||||
self.tab = tab_index
|
||||
self.last_used = datetime.now()
|
||||
self.open = True
|
||||
|
||||
@staticmethod
|
||||
def move_tab(session: scoped_session, frm: int, to: int) -> None:
|
||||
"""Move tabs"""
|
||||
def name_is_available(session: scoped_session, name: str) -> bool:
|
||||
"""
|
||||
Return True if no playlist of this name exists else false.
|
||||
"""
|
||||
|
||||
row_frm = session.execute(select(Playlists).filter_by(tab=frm)).scalar_one()
|
||||
|
||||
row_to = session.execute(select(Playlists).filter_by(tab=to)).scalar_one()
|
||||
|
||||
row_frm.tab = None
|
||||
row_to.tab = None
|
||||
session.commit()
|
||||
row_to.tab = frm
|
||||
row_frm.tab = to
|
||||
return session.execute(
|
||||
select(Playlists)
|
||||
.where(Playlists.name == name)
|
||||
).first() is None
|
||||
|
||||
def rename(self, session: scoped_session, new_name: str) -> None:
|
||||
"""
|
||||
@ -489,17 +477,17 @@ class PlaylistRows(Base):
|
||||
session.flush()
|
||||
|
||||
@staticmethod
|
||||
def delete_rows(
|
||||
session: scoped_session, playlist_id: int, row_numbers: List[int]
|
||||
def delete_row(
|
||||
session: scoped_session, playlist_id: int, row_number: int
|
||||
) -> None:
|
||||
"""
|
||||
Delete passed rows in given playlist.
|
||||
Delete passed row in given playlist.
|
||||
"""
|
||||
|
||||
session.execute(
|
||||
delete(PlaylistRows).where(
|
||||
PlaylistRows.playlist_id == playlist_id,
|
||||
PlaylistRows.plr_rownum.in_(row_numbers),
|
||||
PlaylistRows.plr_rownum == row_number,
|
||||
)
|
||||
)
|
||||
|
||||
@ -684,7 +672,6 @@ class Settings(Base):
|
||||
f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
value = self.f_datetime or self.f_int or self.f_string
|
||||
return (
|
||||
f"<Settings(id={self.id}, name={self.name}, "
|
||||
f"f_datetime={self.f_datetime}, f_int={self.f_int}, f_string={self.f_string}>"
|
||||
|
||||
@ -442,6 +442,14 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if record.f_int != splitter_bottom:
|
||||
record.update(session, {"f_int": splitter_bottom})
|
||||
|
||||
# Save tab number of open playlists
|
||||
for idx in range(self.tabPlaylist.count()):
|
||||
playlist_id = self.tabPlaylist.widget(idx).playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
playlist.tab = idx
|
||||
session.flush()
|
||||
|
||||
# Save current tab
|
||||
record = settings["active_tab"]
|
||||
record.update(session, {"f_int": self.tabPlaylist.currentIndex()})
|
||||
@ -463,29 +471,25 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
Return True if tab closed else False.
|
||||
"""
|
||||
|
||||
return False
|
||||
# TODO Reimplement without ussing self.current_track.playlist_tab
|
||||
# # Don't close current track playlist
|
||||
# if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab):
|
||||
# self.statusbar.showMessage("Can't close current track playlist", 5000)
|
||||
# return False
|
||||
# Don't close current track playlist
|
||||
current_track_playlist_id = track_sequence.now.playlist_id
|
||||
closing_tab_playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
|
||||
if current_track_playlist_id:
|
||||
if closing_tab_playlist_id == current_track_playlist_id:
|
||||
self.statusbar.showMessage("Can't close current track playlist", 5000)
|
||||
return False
|
||||
|
||||
# # Attempt to close next track playlist
|
||||
# if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab:
|
||||
# self.next_track.playlist_tab.clear_next()
|
||||
# Record playlist as closed and update remaining playlist tabs
|
||||
with Session() as session:
|
||||
playlist = session.get(Playlists, closing_tab_playlist_id)
|
||||
if playlist:
|
||||
playlist.close()
|
||||
|
||||
# # Record playlist as closed and update remaining playlist tabs
|
||||
# with Session() as session:
|
||||
# playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
|
||||
# playlist = session.get(Playlists, playlist_id)
|
||||
# if playlist:
|
||||
# playlist.close(session)
|
||||
# Close playlist and remove tab
|
||||
self.tabPlaylist.widget(tab_index).close()
|
||||
self.tabPlaylist.removeTab(tab_index)
|
||||
|
||||
# # Close playlist and remove tab
|
||||
# self.tabPlaylist.widget(tab_index).close()
|
||||
# self.tabPlaylist.removeTab(tab_index)
|
||||
|
||||
# return True
|
||||
return True
|
||||
|
||||
def connect_signals_slots(self) -> None:
|
||||
self.action_About.triggered.connect(self.about)
|
||||
@ -525,7 +529,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
lambda: self.tabPlaylist.currentWidget().lookup_row_in_wikipedia()
|
||||
)
|
||||
self.actionSearch.triggered.connect(self.search_playlist)
|
||||
self.actionSelect_duplicate_rows.triggered.connect(self.select_duplicate_rows)
|
||||
self.actionSelect_duplicate_rows.triggered.connect(
|
||||
lambda: self.active_tab().select_duplicate_rows()
|
||||
)
|
||||
self.actionSelect_next_track.triggered.connect(self.select_next_row)
|
||||
self.actionSelect_previous_track.triggered.connect(self.select_previous_row)
|
||||
self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
|
||||
@ -539,10 +545,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.btnStop.clicked.connect(self.stop)
|
||||
self.hdrCurrentTrack.clicked.connect(self.show_current)
|
||||
self.hdrNextTrack.clicked.connect(self.show_next)
|
||||
self.tabPlaylist.currentChanged.connect(self.tab_change)
|
||||
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
|
||||
self.tabBar = self.tabPlaylist.tabBar()
|
||||
self.tabBar.tabMoved.connect(self.move_tab)
|
||||
self.txtSearch.returnPressed.connect(self.search_playlist_return)
|
||||
|
||||
self.signals.enable_escape_signal.connect(self.enable_escape)
|
||||
@ -557,12 +561,16 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
) -> Optional[Playlists]:
|
||||
"""Create new playlist"""
|
||||
|
||||
playlist_name = self.solicit_playlist_name()
|
||||
playlist_name = self.solicit_playlist_name(session)
|
||||
if not playlist_name:
|
||||
return None
|
||||
playlist = Playlists(session, playlist_name)
|
||||
|
||||
return playlist
|
||||
if playlist:
|
||||
playlist.mark_open()
|
||||
return playlist
|
||||
|
||||
return None
|
||||
|
||||
def create_and_show_playlist(self) -> None:
|
||||
"""Create new playlist and display it"""
|
||||
@ -570,9 +578,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
with Session() as session:
|
||||
playlist = self.create_playlist(session)
|
||||
if playlist:
|
||||
self.create_playlist_tab(session, playlist)
|
||||
self.create_playlist_tab(playlist)
|
||||
|
||||
def create_playlist_tab(self, session: scoped_session, playlist: Playlists) -> int:
|
||||
def create_playlist_tab(self, playlist: Playlists) -> int:
|
||||
"""
|
||||
Take the passed playlist database object, create a playlist tab and
|
||||
add tab to display. Return index number of tab.
|
||||
@ -851,8 +859,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if ok:
|
||||
model.insert_header_row(
|
||||
self.active_tab().get_selected_row_number(), dlg.textValue()
|
||||
model.insert_row(
|
||||
proposed_row_number=self.active_tab().get_selected_row_number(),
|
||||
note=dlg.textValue(),
|
||||
)
|
||||
|
||||
def insert_track(self) -> None:
|
||||
@ -872,7 +881,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
with Session() as session:
|
||||
for playlist in Playlists.get_open(session):
|
||||
if playlist:
|
||||
_ = self.create_playlist_tab(session, playlist)
|
||||
_ = self.create_playlist_tab(playlist)
|
||||
# Set active tab
|
||||
record = Settings.get_int_settings(session, "active_tab")
|
||||
if record.f_int and record.f_int >= 0:
|
||||
@ -966,12 +975,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
self.move_playlist_rows(session, selected_plrs)
|
||||
|
||||
def move_tab(self, frm: int, to: int) -> None:
|
||||
"""Handle tabs being moved"""
|
||||
|
||||
with Session() as session:
|
||||
Playlists.move_tab(session, frm, to)
|
||||
|
||||
def move_unplayed(self) -> None:
|
||||
"""
|
||||
Move unplayed rows to another playlist
|
||||
@ -994,18 +997,22 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
dlg.exec()
|
||||
template = dlg.playlist
|
||||
if template:
|
||||
playlist_name = self.solicit_playlist_name()
|
||||
playlist_name = self.solicit_playlist_name(session)
|
||||
if not playlist_name:
|
||||
return
|
||||
playlist = Playlists.create_playlist_from_template(
|
||||
session, template, playlist_name
|
||||
)
|
||||
if not playlist:
|
||||
return
|
||||
tab_index = self.create_playlist_tab(session, playlist)
|
||||
playlist.mark_open(session, tab_index)
|
||||
|
||||
def open_playlist(self):
|
||||
# Need to ensure that the new playlist is committed to
|
||||
# the database before it is opened by the model.
|
||||
|
||||
session.commit()
|
||||
if playlist:
|
||||
playlist.mark_open()
|
||||
self.create_playlist_tab(playlist)
|
||||
|
||||
def open_playlist(self) -> None:
|
||||
"""Open existing playlist"""
|
||||
|
||||
with Session() as session:
|
||||
@ -1014,8 +1021,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
dlg.exec()
|
||||
playlist = dlg.playlist
|
||||
if playlist:
|
||||
tab_index = self.create_playlist_tab(session, playlist)
|
||||
playlist.mark_open(session, tab_index)
|
||||
self.create_playlist_tab(playlist)
|
||||
playlist.mark_open()
|
||||
|
||||
def paste_rows(self) -> None:
|
||||
"""
|
||||
@ -1291,35 +1298,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.active_tab().set_search(self.txtSearch.text())
|
||||
self.enable_play_next_controls()
|
||||
|
||||
def select_duplicate_rows(self) -> None:
|
||||
"""
|
||||
Select the last of any rows with duplicate tracks in current playlist.
|
||||
This allows the selection to typically come towards the end of the playlist away
|
||||
from any show specific sections.
|
||||
If there a track is selected on three or more rows, only the last one is selected.
|
||||
"""
|
||||
|
||||
visible_playlist_id = self.active_tab().playlist_id
|
||||
# Get row number of duplicate rows
|
||||
sql = text(
|
||||
f"""
|
||||
SELECT max(plr_rownum)
|
||||
FROM playlist_rows
|
||||
WHERE playlist_id = {visible_playlist_id}
|
||||
AND track_id != 0
|
||||
GROUP BY track_id
|
||||
HAVING count(id) > 1
|
||||
"""
|
||||
)
|
||||
|
||||
with Session() as session:
|
||||
row_numbers = [int(a) for a in session.execute(sql).scalars().all()]
|
||||
if row_numbers:
|
||||
self.active_tab().select_rows(row_numbers)
|
||||
self.statusbar.showMessage(
|
||||
f"{len(row_numbers)} duplicate rows selected", 10000
|
||||
)
|
||||
|
||||
def select_next_row(self) -> None:
|
||||
"""Select next or first row in playlist"""
|
||||
|
||||
@ -1386,20 +1364,32 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
|
||||
# self.tabPlaylist.currentWidget().scroll_next_to_top()
|
||||
|
||||
def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]:
|
||||
"""Get name of playlist from user"""
|
||||
def solicit_playlist_name(
|
||||
self, session: scoped_session, default: str = ""
|
||||
) -> Optional[str]:
|
||||
"""Get name of new playlist from user"""
|
||||
|
||||
dlg = QInputDialog(self)
|
||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||
dlg.setLabelText("Playlist name:")
|
||||
if default:
|
||||
dlg.setTextValue(default)
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if ok:
|
||||
return dlg.textValue()
|
||||
else:
|
||||
return None
|
||||
while True:
|
||||
if default:
|
||||
dlg.setTextValue(default)
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if ok:
|
||||
proposed_name = dlg.textValue()
|
||||
if Playlists.name_is_available(session, proposed_name):
|
||||
return proposed_name
|
||||
else:
|
||||
helpers.show_warning(
|
||||
self,
|
||||
"Name in use",
|
||||
f"There's already a playlist called '{proposed_name}'",
|
||||
)
|
||||
continue
|
||||
else:
|
||||
return None
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop playing immediately"""
|
||||
@ -1463,15 +1453,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# Enable controls
|
||||
self.enable_play_next_controls()
|
||||
|
||||
def tab_change(self):
|
||||
"""Called when active tab changed"""
|
||||
|
||||
try:
|
||||
self.tabPlaylist.currentWidget().tab_visible()
|
||||
except AttributeError:
|
||||
# May also be called when last tab is closed
|
||||
pass
|
||||
|
||||
def set_next_plr_id(
|
||||
self, next_plr_id: Optional[int], playlist_tab: PlaylistTab
|
||||
) -> None:
|
||||
|
||||
@ -124,6 +124,9 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.signals.add_track_to_playlist_signal.connect(self.add_track)
|
||||
|
||||
with Session() as session:
|
||||
# Ensure row numbers in playlist are contiguous
|
||||
PlaylistRows.fixup_rownumbers(session, playlist_id)
|
||||
# Populate self.playlist_rows
|
||||
self.refresh_data(session)
|
||||
self.update_track_times()
|
||||
|
||||
@ -147,15 +150,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if playlist_id != self.playlist_id:
|
||||
return
|
||||
|
||||
# Insert track if we have one
|
||||
if track_id:
|
||||
self.insert_track_row(new_row_number, track_id, note)
|
||||
# If we only have a note, add as a header row
|
||||
elif note:
|
||||
self.insert_header_row(new_row_number, note)
|
||||
else:
|
||||
# No track, no note, no point
|
||||
return
|
||||
self.insert_row(proposed_row_number=new_row_number, track_id=track_id, note=note)
|
||||
|
||||
def add_track_to_header(
|
||||
self,
|
||||
@ -345,10 +340,17 @@ class PlaylistModel(QAbstractTableModel):
|
||||
def delete_rows(self, row_numbers: List[int]) -> None:
|
||||
"""
|
||||
Delete passed rows from model
|
||||
|
||||
Need to delete them in contiguous groups wrapped in beginRemoveRows / endRemoveRows
|
||||
calls. To keep it simple, if inefficient, delete rows one by one.
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
PlaylistRows.delete_rows(session, self.playlist_id, row_numbers)
|
||||
for row_number in row_numbers:
|
||||
super().beginRemoveRows(QModelIndex(), row_number, row_number)
|
||||
PlaylistRows.delete_row(session, self.playlist_id, row_number)
|
||||
super().endRemoveRows()
|
||||
|
||||
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
|
||||
self.refresh_data(session)
|
||||
self.update_track_times()
|
||||
@ -397,6 +399,26 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return QVariant()
|
||||
|
||||
def get_duplicate_rows(self) -> List[int]:
|
||||
"""
|
||||
Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4]
|
||||
(ie, ignore the first, not-yet-duplicate, track).
|
||||
"""
|
||||
|
||||
found = []
|
||||
result = []
|
||||
|
||||
for i in range(len(self.playlist_rows)):
|
||||
track_id = self.playlist_rows[i].track_id
|
||||
if track_id is None:
|
||||
continue
|
||||
if track_id in found:
|
||||
result.append(i)
|
||||
else:
|
||||
found.append(track_id)
|
||||
|
||||
return result
|
||||
|
||||
def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
||||
"""
|
||||
Return text for editing
|
||||
@ -448,6 +470,25 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return QVariant(boldfont)
|
||||
|
||||
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
|
||||
"""
|
||||
Sanitises proposed new row number.
|
||||
|
||||
If proposed_row_number given, ensure it is valid.
|
||||
If not given, return row number to add to end of model.
|
||||
"""
|
||||
|
||||
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
|
||||
# We are adding to the end of the list
|
||||
new_row_number = len(self.playlist_rows)
|
||||
elif proposed_row_number < 0:
|
||||
# Add to start of list
|
||||
new_row_number = 0
|
||||
else:
|
||||
new_row_number = proposed_row_number
|
||||
|
||||
return new_row_number
|
||||
|
||||
def get_row_track_path(self, row_number: int) -> str:
|
||||
"""
|
||||
Return path of track associated with row or empty string if no track associated
|
||||
@ -466,6 +507,13 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return duration
|
||||
|
||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||
"""
|
||||
Return info about passed row
|
||||
"""
|
||||
|
||||
return self.playlist_rows[row_number]
|
||||
|
||||
def headerData(
|
||||
self,
|
||||
section: int,
|
||||
@ -600,66 +648,33 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return self.playlist_rows[row_number].played
|
||||
|
||||
def insert_header_row(self, row_number: Optional[int], text: str) -> None:
|
||||
"""
|
||||
Insert a header row.
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
plr = self._insert_row(session, row_number)
|
||||
# Update the PlaylistRows object
|
||||
plr.note = text
|
||||
# Repopulate self.playlist_rows
|
||||
self.refresh_data(session)
|
||||
# Update the display from the new row onwards
|
||||
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
|
||||
|
||||
def _insert_row(
|
||||
self, session: scoped_session, row_number: Optional[int]
|
||||
def insert_row(
|
||||
self,
|
||||
proposed_row_number: Optional[int],
|
||||
track_id: Optional[int] = None,
|
||||
note: Optional[str] = None,
|
||||
) -> PlaylistRows:
|
||||
"""
|
||||
Insert a row in the database.
|
||||
|
||||
If row_number is greater than length of list plus 1, or if row
|
||||
number is None, put row at end of list.
|
||||
|
||||
Move existing rows to make space if ncessary.
|
||||
|
||||
Return the new PlaylistRows object.
|
||||
"""
|
||||
|
||||
if row_number is None or row_number > len(self.playlist_rows):
|
||||
# We are adding to the end of the list so we can optimise
|
||||
new_row_number = len(self.playlist_rows)
|
||||
return PlaylistRows(session, self.playlist_id, new_row_number)
|
||||
elif row_number < 0:
|
||||
raise ValueError(
|
||||
f"playlistmodel._insert_row, invalid row number ({row_number})"
|
||||
)
|
||||
else:
|
||||
new_row_number = row_number
|
||||
|
||||
# Insert the new row and return it
|
||||
return PlaylistRows.insert_row(session, self.playlist_id, new_row_number)
|
||||
|
||||
def insert_track_row(
|
||||
self, row_number: Optional[int], track_id: int, text: Optional[str]
|
||||
) -> None:
|
||||
"""
|
||||
Insert a track row.
|
||||
"""
|
||||
|
||||
new_row_number = self._get_new_row_number(proposed_row_number)
|
||||
|
||||
with Session() as session:
|
||||
plr = self._insert_row(session, row_number)
|
||||
# Update the PlaylistRows object
|
||||
super().beginInsertRows(QModelIndex(), new_row_number, new_row_number)
|
||||
plr = PlaylistRows.insert_row(session, self.playlist_id, new_row_number)
|
||||
|
||||
plr.track_id = track_id
|
||||
if text:
|
||||
plr.note = text
|
||||
# Repopulate self.playlist_rows
|
||||
if note:
|
||||
plr.note = note
|
||||
|
||||
self.refresh_data(session)
|
||||
# Update the display from the new row onwards
|
||||
super().endInsertRows()
|
||||
|
||||
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
|
||||
|
||||
return plr
|
||||
|
||||
def invalidate_row(self, modified_row: int) -> None:
|
||||
"""
|
||||
Signal to view to refresh invlidated row
|
||||
|
||||
@ -205,7 +205,26 @@ class PlaylistTab(QTableView):
|
||||
self.setModel(PlaylistModel(playlist_id))
|
||||
self._set_column_widths()
|
||||
|
||||
# ########## Events other than cell editing ##########
|
||||
def closeEditor(
|
||||
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
|
||||
) -> None:
|
||||
"""
|
||||
Override closeEditor to enable play controls and update display.
|
||||
"""
|
||||
|
||||
self.musicmuster.enable_play_next_controls()
|
||||
self.musicmuster.actionSetNext.setEnabled(True)
|
||||
self.musicmuster.action_Clear_selection.setEnabled(True)
|
||||
|
||||
super(PlaylistTab, self).closeEditor(editor, hint)
|
||||
|
||||
# Optimise row heights after increasing row height for editing
|
||||
self.resizeRowsToContents()
|
||||
|
||||
# Update start times in case a start time in a note has been
|
||||
# edited
|
||||
model = cast(PlaylistModel, self.model())
|
||||
model.update_track_times()
|
||||
|
||||
def dropEvent(self, event):
|
||||
if event.source() is not self or (
|
||||
@ -230,6 +249,25 @@ class PlaylistTab(QTableView):
|
||||
|
||||
event.accept()
|
||||
|
||||
def edit(
|
||||
self,
|
||||
index: QModelIndex,
|
||||
trigger: QAbstractItemView.EditTrigger,
|
||||
event: Optional[QEvent],
|
||||
) -> bool:
|
||||
"""
|
||||
Override PySide2.QAbstractItemView.edit to catch when editing starts
|
||||
|
||||
Editing only ever starts with a double click on a cell
|
||||
"""
|
||||
|
||||
# 'result' will only be true on double-click
|
||||
result = super().edit(index, trigger, event)
|
||||
if result:
|
||||
self.musicmuster.disable_play_next_controls()
|
||||
|
||||
return result
|
||||
|
||||
def _add_context_menu(
|
||||
self,
|
||||
text: str,
|
||||
@ -538,9 +576,9 @@ class PlaylistTab(QTableView):
|
||||
parent_menu=sort_menu,
|
||||
)
|
||||
|
||||
# Info TODO
|
||||
# Info
|
||||
if track_row:
|
||||
self._add_context_menu("Info", lambda: print("Track info"))
|
||||
self._add_context_menu("Info", lambda: self._info_row(row_number))
|
||||
|
||||
# Track path TODO
|
||||
if track_row:
|
||||
@ -644,25 +682,23 @@ class PlaylistTab(QTableView):
|
||||
# items in that row selected)
|
||||
return sorted(list(set([a.row() for a in self.selectedIndexes()])))
|
||||
|
||||
def _info_row(self, track_id: int) -> None:
|
||||
def _info_row(self, row_number: int) -> None:
|
||||
"""Display popup with info re row"""
|
||||
|
||||
with Session() as session:
|
||||
track = session.get(Tracks, track_id)
|
||||
if track:
|
||||
txt = (
|
||||
f"Title: {track.title}\n"
|
||||
f"Artist: {track.artist}\n"
|
||||
f"Track ID: {track.id}\n"
|
||||
f"Track duration: {ms_to_mmss(track.duration)}\n"
|
||||
f"Track bitrate: {track.bitrate}\n"
|
||||
f"Track fade at: {ms_to_mmss(track.fade_at)}\n"
|
||||
f"Track silence at: {ms_to_mmss(track.silence_at)}"
|
||||
"\n\n"
|
||||
f"Path: {track.path}\n"
|
||||
)
|
||||
else:
|
||||
txt = f"Can't find {track_id=}"
|
||||
model = cast(PlaylistModel, self.model())
|
||||
prd = model.get_row_info(row_number)
|
||||
if prd:
|
||||
txt = (
|
||||
f"Title: {prd.title}\n"
|
||||
f"Artist: {prd.artist}\n"
|
||||
f"Track ID: {prd.track_id}\n"
|
||||
f"Track duration: {ms_to_mmss(prd.duration)}\n"
|
||||
f"Track bitrate: {prd.bitrate}\n"
|
||||
"\n\n"
|
||||
f"Path: {prd.path}\n"
|
||||
)
|
||||
else:
|
||||
txt = f"Can't find info about row{row_number}"
|
||||
|
||||
info: QMessageBox = QMessageBox(self)
|
||||
info.setIcon(QMessageBox.Icon.Information)
|
||||
@ -879,6 +915,26 @@ class PlaylistTab(QTableView):
|
||||
# if match_row is not None:
|
||||
# self.selectRow(row_number)
|
||||
|
||||
def select_duplicate_rows(self) -> None:
|
||||
"""
|
||||
Select the last of any rows with duplicate tracks in current playlist.
|
||||
This allows the selection to typically come towards the end of the playlist away
|
||||
from any show specific sections.
|
||||
"""
|
||||
|
||||
# Clear any selected rows to avoid confustion
|
||||
self.clear_selection()
|
||||
# We need to be in MultiSelection mode
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
||||
# Get the duplicate rows
|
||||
model = cast(PlaylistModel, self.model())
|
||||
duplicate_rows = model.get_duplicate_rows()
|
||||
# Select the rows
|
||||
for duplicate_row in duplicate_rows:
|
||||
self.selectRow(duplicate_row)
|
||||
# Reset selection mode
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
def selectionChanged(
|
||||
self, selected: QItemSelection, deselected: QItemSelection
|
||||
) -> None:
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
"""Add 'open' field to Playlists
|
||||
|
||||
Revision ID: 5bb2c572e1e5
|
||||
Revises: 3a53a9fb26ab
|
||||
Create Date: 2023-11-18 14:19:02.643914
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5bb2c572e1e5'
|
||||
down_revision = '3a53a9fb26ab'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('carts', 'duration',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('carts', 'path',
|
||||
existing_type=mysql.VARCHAR(length=2048),
|
||||
nullable=True)
|
||||
op.alter_column('carts', 'enabled',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
nullable=True)
|
||||
op.alter_column('playlist_rows', 'note',
|
||||
existing_type=mysql.VARCHAR(length=2048),
|
||||
nullable=False)
|
||||
op.add_column('playlists', sa.Column('open', sa.Boolean(), nullable=False))
|
||||
op.alter_column('settings', 'name',
|
||||
existing_type=mysql.VARCHAR(length=32),
|
||||
type_=sa.String(length=64),
|
||||
existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('settings', 'name',
|
||||
existing_type=sa.String(length=64),
|
||||
type_=mysql.VARCHAR(length=32),
|
||||
existing_nullable=False)
|
||||
op.drop_column('playlists', 'open')
|
||||
op.alter_column('playlist_rows', 'note',
|
||||
existing_type=mysql.VARCHAR(length=2048),
|
||||
nullable=True)
|
||||
op.alter_column('carts', 'enabled',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
nullable=False)
|
||||
op.alter_column('carts', 'path',
|
||||
existing_type=mysql.VARCHAR(length=2048),
|
||||
nullable=False)
|
||||
op.alter_column('carts', 'duration',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
@ -2,6 +2,8 @@ from app.models import (
|
||||
Playlists,
|
||||
Tracks,
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from app.helpers import get_file_metadata
|
||||
from app import playlistmodel
|
||||
from dbconfig import scoped_session
|
||||
@ -25,7 +27,7 @@ def create_model_with_tracks(session: scoped_session) -> "playlistmodel.Playlist
|
||||
track_path = test_tracks[row % len(test_tracks)]
|
||||
metadata = get_file_metadata(track_path)
|
||||
track = Tracks(session, **metadata)
|
||||
model.insert_track_row(row, track.id, f"{row=}")
|
||||
model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}")
|
||||
|
||||
session.commit()
|
||||
return model
|
||||
@ -38,10 +40,8 @@ def create_model_with_playlist_rows(
|
||||
# Create a model
|
||||
model = playlistmodel.PlaylistModel(playlist.id)
|
||||
for row in range(rows):
|
||||
plr = model._insert_row(session, row)
|
||||
newrow = plr.plr_rownum
|
||||
plr.note = str(newrow)
|
||||
model.playlist_rows[newrow] = playlistmodel.PlaylistRowData(plr)
|
||||
plr = model.insert_row(proposed_row_number=row, note=str(row))
|
||||
model.playlist_rows[plr.plr_rownum] = playlistmodel.PlaylistRowData(plr)
|
||||
|
||||
session.commit()
|
||||
return model
|
||||
@ -195,7 +195,7 @@ def test_insert_header_row_end(monkeypatch, session):
|
||||
initial_row_count = 11
|
||||
|
||||
model = create_model_with_playlist_rows(session, initial_row_count)
|
||||
model.insert_header_row(None, note_text)
|
||||
model.insert_row(proposed_row_number=None, note=note_text)
|
||||
assert model.rowCount() == initial_row_count + 1
|
||||
prd = model.playlist_rows[model.rowCount() - 1]
|
||||
# Test against edit_role because display_role for headers is
|
||||
@ -215,7 +215,7 @@ def test_insert_header_row_middle(monkeypatch, session):
|
||||
insert_row = 6
|
||||
|
||||
model = create_model_with_playlist_rows(session, initial_row_count)
|
||||
model.insert_header_row(insert_row, note_text)
|
||||
model.insert_row(proposed_row_number=insert_row, note=note_text)
|
||||
assert model.rowCount() == initial_row_count + 1
|
||||
prd = model.playlist_rows[insert_row]
|
||||
# Test against edit_role because display_role for headers is
|
||||
@ -239,14 +239,35 @@ def test_timing_one_track(monkeypatch, session):
|
||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
||||
model = create_model_with_tracks(session)
|
||||
|
||||
model.insert_header_row(START_ROW, "start+")
|
||||
model.insert_header_row(END_ROW, "-")
|
||||
model.insert_row(proposed_row_number=START_ROW, note="start+")
|
||||
model.insert_row(proposed_row_number=END_ROW, note="-")
|
||||
|
||||
prd = model.playlist_rows[START_ROW]
|
||||
qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd)
|
||||
assert qv_value.value() == "start [1 tracks, 4:23 unplayed]"
|
||||
|
||||
|
||||
def test_insert_track_new_playlist(monkeypatch, session):
|
||||
# insert a track into a new playlist
|
||||
|
||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
||||
|
||||
playlist = Playlists(session, "test playlist")
|
||||
# Create a model
|
||||
model = playlistmodel.PlaylistModel(playlist.id)
|
||||
|
||||
track_path = test_tracks[0]
|
||||
metadata = get_file_metadata(track_path)
|
||||
track = Tracks(session, **metadata)
|
||||
model.insert_row(proposed_row_number=0, track_id=track.id)
|
||||
|
||||
prd = model.playlist_rows[model.rowCount() - 1]
|
||||
assert (
|
||||
model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd)
|
||||
== metadata["title"]
|
||||
)
|
||||
|
||||
|
||||
# def test_edit_header(monkeypatch, session): # edit header row in middle of playlist
|
||||
|
||||
# monkeypatch.setattr(playlistmodel, "Session", session)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user