Compare commits
No commits in common. "0f1d5117cc14d619de6df2c6db2c539b82c1a038" and "04f0e95653c6ce95e8eb3f0a318e57087c21f9e1" have entirely different histories.
0f1d5117cc
...
04f0e95653
@ -79,11 +79,12 @@ class MusicMusterSignals(QObject):
|
|||||||
https://refactoring.guru/design-patterns/singleton/python/example#example-0
|
https://refactoring.guru/design-patterns/singleton/python/example#example-0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
add_track_to_header_signal = pyqtSignal(int, int, int)
|
||||||
|
add_track_to_playlist_signal = pyqtSignal(int, int, int, str)
|
||||||
begin_reset_model_signal = pyqtSignal(int)
|
begin_reset_model_signal = pyqtSignal(int)
|
||||||
enable_escape_signal = pyqtSignal(bool)
|
enable_escape_signal = pyqtSignal(bool)
|
||||||
end_reset_model_signal = pyqtSignal(int)
|
end_reset_model_signal = pyqtSignal(int)
|
||||||
next_track_changed_signal = pyqtSignal()
|
next_track_changed_signal = pyqtSignal()
|
||||||
resize_rows_signal = pyqtSignal(int)
|
|
||||||
row_order_changed_signal = pyqtSignal(int)
|
row_order_changed_signal = pyqtSignal(int)
|
||||||
search_songfacts_signal = pyqtSignal(str)
|
search_songfacts_signal = pyqtSignal(str)
|
||||||
search_wikipedia_signal = pyqtSignal(str)
|
search_wikipedia_signal = pyqtSignal(str)
|
||||||
|
|||||||
@ -6,12 +6,10 @@ from PyQt6.QtWidgets import QDialog, QListWidgetItem
|
|||||||
from classes import MusicMusterSignals
|
from classes import MusicMusterSignals
|
||||||
from dbconfig import scoped_session
|
from dbconfig import scoped_session
|
||||||
from helpers import (
|
from helpers import (
|
||||||
ask_yes_no,
|
|
||||||
get_relative_date,
|
get_relative_date,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
)
|
)
|
||||||
from models import Settings, Tracks
|
from models import Settings, Tracks
|
||||||
from playlistmodel import PlaylistModel
|
|
||||||
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
|
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +20,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: scoped_session,
|
||||||
new_row_number: int,
|
new_row_number: int,
|
||||||
model: PlaylistModel,
|
playlist_id: int,
|
||||||
add_to_header: Optional[bool] = False,
|
add_to_header: Optional[bool] = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -34,7 +32,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.session = session
|
self.session = session
|
||||||
self.new_row_number = new_row_number
|
self.new_row_number = new_row_number
|
||||||
self.model = model
|
self.playlist_id = playlist_id
|
||||||
self.add_to_header = add_to_header
|
self.add_to_header = add_to_header
|
||||||
self.ui = Ui_Dialog()
|
self.ui = Ui_Dialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
@ -75,30 +73,14 @@ class TrackSelectDialog(QDialog):
|
|||||||
track_id = None
|
track_id = None
|
||||||
if track:
|
if track:
|
||||||
track_id = track.id
|
track_id = track.id
|
||||||
|
if self.add_to_header:
|
||||||
|
self.signals.add_track_to_header_signal.emit(
|
||||||
|
self.playlist_id, self.new_row_number, track_id
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return
|
self.signals.add_track_to_playlist_signal.emit(
|
||||||
# Check whether track is already in playlist
|
self.playlist_id, self.new_row_number, track_id, note
|
||||||
move_existing = False
|
)
|
||||||
existing_prd = self.model.is_track_in_playlist(track_id)
|
|
||||||
if existing_prd is not None:
|
|
||||||
if ask_yes_no(
|
|
||||||
"Duplicate row",
|
|
||||||
"Track already in playlist. " "Move to new location?",
|
|
||||||
default_yes=True,
|
|
||||||
):
|
|
||||||
move_existing = True
|
|
||||||
if self.add_to_header and existing_prd: # and existing_prd for mypy's benefit
|
|
||||||
if move_existing:
|
|
||||||
self.model.move_track_to_header(self.new_row_number, existing_prd, note)
|
|
||||||
else:
|
|
||||||
self.model.add_track_to_header(self.new_row_number, track_id)
|
|
||||||
# Close dialog - we can only add one track to a header
|
|
||||||
self.accept()
|
|
||||||
else:
|
|
||||||
if move_existing and existing_prd: # and existing_prd for mypy's benefit
|
|
||||||
self.model.move_track_add_note(self.new_row_number, existing_prd, note)
|
|
||||||
else:
|
|
||||||
self.model.insert_row(self.new_row_number, track_id, note)
|
|
||||||
|
|
||||||
def add_selected_and_close(self) -> None:
|
def add_selected_and_close(self) -> None:
|
||||||
"""Handle Add and Close button"""
|
"""Handle Add and Close button"""
|
||||||
|
|||||||
@ -148,11 +148,11 @@ class ImportTrack(QObject):
|
|||||||
import_finished = pyqtSignal()
|
import_finished = pyqtSignal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, filenames: List[str], model: PlaylistModel, row_number: Optional[int]
|
self, filenames: List[str], playlist_id: int, row_number: Optional[int]
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.filenames = filenames
|
self.filenames = filenames
|
||||||
self.model = model
|
self.playlist_id = playlist_id
|
||||||
self.next_row_number = row_number
|
self.next_row_number = row_number
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
|
|
||||||
@ -179,7 +179,9 @@ class ImportTrack(QObject):
|
|||||||
# previous additions in this loop. So, commit now to
|
# previous additions in this loop. So, commit now to
|
||||||
# lock in what we've just done.
|
# lock in what we've just done.
|
||||||
session.commit()
|
session.commit()
|
||||||
self.model.insert_row(self.next_row_number, track.id, "")
|
self.signals.add_track_to_playlist_signal.emit(
|
||||||
|
self.playlist_id, self.next_row_number, track.id, ""
|
||||||
|
)
|
||||||
self.next_row_number += 1
|
self.next_row_number += 1
|
||||||
self.signals.status_message_signal.emit(
|
self.signals.status_message_signal.emit(
|
||||||
f"{len(self.filenames)} tracks imported", 10000
|
f"{len(self.filenames)} tracks imported", 10000
|
||||||
@ -530,6 +532,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.actionSelect_duplicate_rows.triggered.connect(
|
self.actionSelect_duplicate_rows.triggered.connect(
|
||||||
lambda: self.active_tab().select_duplicate_rows()
|
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)
|
self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
|
||||||
self.actionSetNext.triggered.connect(self.set_selected_track_next)
|
self.actionSetNext.triggered.connect(self.set_selected_track_next)
|
||||||
self.actionSkipToNext.triggered.connect(self.play_next)
|
self.actionSkipToNext.triggered.connect(self.play_next)
|
||||||
@ -828,8 +832,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.import_thread = QThread()
|
self.import_thread = QThread()
|
||||||
self.worker = ImportTrack(
|
self.worker = ImportTrack(
|
||||||
new_tracks,
|
new_tracks,
|
||||||
self.active_model(),
|
self.active_tab().playlist_id,
|
||||||
self.active_tab().selected_model_row_number(),
|
self.active_tab().get_selected_row_number(),
|
||||||
)
|
)
|
||||||
self.worker.moveToThread(self.import_thread)
|
self.worker.moveToThread(self.import_thread)
|
||||||
self.import_thread.started.connect(self.worker.run)
|
self.import_thread.started.connect(self.worker.run)
|
||||||
@ -867,8 +871,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
with Session() as session:
|
with Session() as session:
|
||||||
dlg = TrackSelectDialog(
|
dlg = TrackSelectDialog(
|
||||||
session=session,
|
session=session,
|
||||||
new_row_number=self.active_tab().selected_model_row_number(),
|
new_row_number=self.active_tab().get_selected_row_number(),
|
||||||
model=self.active_model(),
|
playlist_id=self.active_tab().playlist_id,
|
||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
@ -889,7 +893,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Display songfacts page for title in highlighted row
|
Display songfacts page for title in highlighted row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
row_number = self.active_tab().selected_model_row_number()
|
row_number = self.active_tab().get_selected_row_number()
|
||||||
if row_number is None:
|
if row_number is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -904,7 +908,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Display Wikipedia page for title in highlighted row
|
Display Wikipedia page for title in highlighted row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
row_number = self.active_tab().selected_model_row_number()
|
row_number = self.active_tab().get_selected_row_number()
|
||||||
if row_number is None:
|
if row_number is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1067,6 +1071,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# Clear next track
|
# Clear next track
|
||||||
self.clear_next()
|
self.clear_next()
|
||||||
|
|
||||||
|
# Set current track playlist_tab colour
|
||||||
|
# TODO Reimplement without reference to self.current_track.playlist_tab
|
||||||
|
# current_tab = self.current_track.playlist_tab
|
||||||
|
# if current_tab:
|
||||||
|
# self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
|
||||||
|
|
||||||
# Restore volume if -3dB active
|
# Restore volume if -3dB active
|
||||||
if self.btnDrop3db.isChecked():
|
if self.btnDrop3db.isChecked():
|
||||||
self.btnDrop3db.setChecked(False)
|
self.btnDrop3db.setChecked(False)
|
||||||
@ -1218,6 +1228,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
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
|
||||||
|
self.active_tab().set_search("")
|
||||||
# Clean up search bar
|
# Clean up search bar
|
||||||
self.txtSearch.setText("")
|
self.txtSearch.setText("")
|
||||||
self.txtSearch.setHidden(True)
|
self.txtSearch.setHidden(True)
|
||||||
@ -1286,7 +1298,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def show_current(self) -> None:
|
def show_current(self) -> None:
|
||||||
"""Scroll to show current track"""
|
"""Scroll to show current track"""
|
||||||
|
|
||||||
self.show_track(track_sequence.now)
|
return
|
||||||
|
# TODO Reimplement
|
||||||
|
# if self.current_track.playlist_tab != self.active_tab():
|
||||||
|
# self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab)
|
||||||
|
# self.tabPlaylist.currentWidget().scroll_current_to_top()
|
||||||
|
|
||||||
def show_warning(self, title: str, body: str) -> None:
|
def show_warning(self, title: str, body: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1299,7 +1315,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def show_next(self) -> None:
|
def show_next(self) -> None:
|
||||||
"""Scroll to show next track"""
|
"""Scroll to show next track"""
|
||||||
|
|
||||||
self.show_track(track_sequence.next)
|
return
|
||||||
|
# TODO Reimplement
|
||||||
|
# if self.next_track.playlist_tab != self.active_tab():
|
||||||
|
# self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
|
||||||
|
# self.tabPlaylist.currentWidget().scroll_next_to_top()
|
||||||
|
|
||||||
def show_status_message(self, message: str, timing: int) -> None:
|
def show_status_message(self, message: str, timing: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1308,23 +1328,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
self.statusbar.showMessage(message, timing)
|
self.statusbar.showMessage(message, timing)
|
||||||
|
|
||||||
def show_track(self, plt: PlaylistTrack) -> None:
|
|
||||||
"""Scroll to show track in plt"""
|
|
||||||
|
|
||||||
# Switch to the correct tab
|
|
||||||
plt_playlist_id = plt.playlist_id
|
|
||||||
if not plt_playlist_id:
|
|
||||||
# No playlist
|
|
||||||
return
|
|
||||||
|
|
||||||
if plt_playlist_id != self.active_tab().playlist_id:
|
|
||||||
for idx in range(self.tabPlaylist.count()):
|
|
||||||
if self.tabPlaylist.widget(idx).playlist_id == plt_playlist_id:
|
|
||||||
self.tabPlaylist.setCurrentIndex(idx)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.tabPlaylist.currentWidget().scroll_to_top(plt.plr_rownum)
|
|
||||||
|
|
||||||
def solicit_playlist_name(
|
def solicit_playlist_name(
|
||||||
self, session: scoped_session, default: str = ""
|
self, session: scoped_session, default: str = ""
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
@ -1414,6 +1417,85 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# Enable controls
|
# Enable controls
|
||||||
self.enable_play_next_controls()
|
self.enable_play_next_controls()
|
||||||
|
|
||||||
|
def set_next_plr_id(
|
||||||
|
self, next_plr_id: Optional[int], playlist_tab: PlaylistTab
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set passed plr_id as next track to play, or clear next track if None
|
||||||
|
|
||||||
|
Actions required:
|
||||||
|
- Update playing_track
|
||||||
|
- Tell playlist tabs to update their 'next track' highlighting
|
||||||
|
- Update headers
|
||||||
|
- Set playlist tab colours
|
||||||
|
- Populate ‘info’ tabs
|
||||||
|
"""
|
||||||
|
|
||||||
|
return
|
||||||
|
# with Session() as session:
|
||||||
|
# # Update self.next_track PlaylistTrack structure
|
||||||
|
# self.next_track = NextTrack()
|
||||||
|
# if next_plr_id:
|
||||||
|
# next_plr = session.get(PlaylistRows, next_plr_id)
|
||||||
|
# if next_plr:
|
||||||
|
# self.next_track.set_plr(session, next_plr)
|
||||||
|
# self.signals.set_next_track_signal.emit(next_plr.playlist_id)
|
||||||
|
|
||||||
|
# # Update headers
|
||||||
|
# self.update_headers()
|
||||||
|
|
||||||
|
# TODO: reimlement
|
||||||
|
# # Set playlist tab colours
|
||||||
|
# self._set_next_track_playlist_tab_colours(old_next_track)
|
||||||
|
|
||||||
|
# if next_plr_id:
|
||||||
|
# # Populate 'info' tabs with Wikipedia info, but queue it
|
||||||
|
# # because it isn't quick
|
||||||
|
# if self.next_track.title:
|
||||||
|
# QTimer.singleShot(
|
||||||
|
# 0,
|
||||||
|
# lambda: self.tabInfolist.open_in_wikipedia(
|
||||||
|
# self.next_track.title
|
||||||
|
# ),
|
||||||
|
# )
|
||||||
|
|
||||||
|
def _set_next_track_playlist_tab_colours(
|
||||||
|
self, old_next_track: Optional[PlaylistTrack]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set playlist tab colour for next track. self.next_track needs
|
||||||
|
to be set before calling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If the original next playlist tab isn't the same as the
|
||||||
|
# new one or the current track, it needs its colour reset.
|
||||||
|
return
|
||||||
|
# TODO Reimplement
|
||||||
|
# if (
|
||||||
|
# old_next_track
|
||||||
|
# and old_next_track.playlist_tab
|
||||||
|
# and old_next_track.playlist_tab
|
||||||
|
# not in [self.next_track.playlist_tab, self.current_track.playlist_tab]
|
||||||
|
# ):
|
||||||
|
# self.set_tab_colour(
|
||||||
|
# old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
|
||||||
|
# )
|
||||||
|
# # If the new next playlist tab isn't the same as the
|
||||||
|
# # old one or the current track, it needs its colour set.
|
||||||
|
# if old_next_track:
|
||||||
|
# old_tab = old_next_track.playlist_tab
|
||||||
|
# else:
|
||||||
|
# old_tab = None
|
||||||
|
# if (
|
||||||
|
# self.next_track
|
||||||
|
# and self.next_track.playlist_tab
|
||||||
|
# and self.next_track.playlist_tab
|
||||||
|
# not in [old_tab, self.current_track.playlist_tab]
|
||||||
|
# ):
|
||||||
|
# self.set_tab_colour(
|
||||||
|
# self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
|
||||||
|
# )
|
||||||
|
|
||||||
def tick_10ms(self) -> None:
|
def tick_10ms(self) -> None:
|
||||||
"""
|
"""
|
||||||
Called every 10ms
|
Called every 10ms
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import obsws_python as obs # type: ignore
|
|
||||||
import re
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import auto, Enum
|
from enum import auto, Enum
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from typing import List, Optional
|
from typing import cast, List, Optional
|
||||||
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QAbstractTableModel,
|
QAbstractTableModel,
|
||||||
@ -37,7 +35,6 @@ from models import Playdates, PlaylistRows, Tracks
|
|||||||
|
|
||||||
|
|
||||||
HEADER_NOTES_COLUMN = 1
|
HEADER_NOTES_COLUMN = 1
|
||||||
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
|
|
||||||
|
|
||||||
|
|
||||||
class Col(Enum):
|
class Col(Enum):
|
||||||
@ -127,6 +124,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
self.played_tracks_hidden = False
|
self.played_tracks_hidden = False
|
||||||
|
|
||||||
|
self.signals.add_track_to_header_signal.connect(self.add_track_to_header)
|
||||||
|
self.signals.add_track_to_playlist_signal.connect(self.add_track)
|
||||||
self.signals.begin_reset_model_signal.connect(self.begin_reset_model)
|
self.signals.begin_reset_model_signal.connect(self.begin_reset_model)
|
||||||
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
||||||
self.signals.row_order_changed_signal.connect(self.row_order_changed)
|
self.signals.row_order_changed_signal.connect(self.row_order_changed)
|
||||||
@ -143,19 +142,45 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
f"<PlaylistModel: playlist_id={self.playlist_id}, {self.rowCount()} rows>"
|
f"<PlaylistModel: playlist_id={self.playlist_id}, {self.rowCount()} rows>"
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_track_to_header(
|
def add_track(
|
||||||
self, row_number: int, track_id: int, note: Optional[str] = None
|
self,
|
||||||
|
playlist_id: int,
|
||||||
|
new_row_number: int,
|
||||||
|
track_id: Optional[int],
|
||||||
|
note: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Add track to existing header row
|
Add track if it's for our playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Ignore if it's not for us
|
||||||
|
if playlist_id != self.playlist_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.insert_row(
|
||||||
|
proposed_row_number=new_row_number, track_id=track_id, note=note
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_track_to_header(
|
||||||
|
self,
|
||||||
|
playlist_id: int,
|
||||||
|
row_number: int,
|
||||||
|
track_id: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add track to existing header row if it's for our playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Ignore if it's not for us
|
||||||
|
if playlist_id != self.playlist_id:
|
||||||
|
return
|
||||||
|
|
||||||
# Get existing row
|
# Get existing row
|
||||||
try:
|
try:
|
||||||
prd = self.playlist_rows[row_number]
|
prd = self.playlist_rows[row_number]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.error(
|
log.error(
|
||||||
f"KeyError in PlaylistModel:add_track_to_header "
|
f"KeyError in PlaylistModel:add_track_to_header ({playlist_id=}, "
|
||||||
f"{row_number=}, {track_id=}, {len(self.playlist_rows)=}"
|
f"{row_number=}, {track_id=}, {len(self.playlist_rows)=}"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -170,9 +195,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if plr:
|
if plr:
|
||||||
# Add track to PlaylistRows
|
# Add track to PlaylistRows
|
||||||
plr.track_id = track_id
|
plr.track_id = track_id
|
||||||
# Add any further note
|
|
||||||
if note:
|
|
||||||
plr.note += "\n" + note
|
|
||||||
# Reset header row spanning
|
# Reset header row spanning
|
||||||
self.signals.span_cells_signal.emit(
|
self.signals.span_cells_signal.emit(
|
||||||
row_number, HEADER_NOTES_COLUMN, 1, 1
|
row_number, HEADER_NOTES_COLUMN, 1, 1
|
||||||
@ -182,8 +204,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Repaint row
|
# Repaint row
|
||||||
self.invalidate_row(row_number)
|
self.invalidate_row(row_number)
|
||||||
|
|
||||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
|
||||||
|
|
||||||
def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush:
|
def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush:
|
||||||
"""Return background setting"""
|
"""Return background setting"""
|
||||||
|
|
||||||
@ -236,7 +256,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
Actions required:
|
Actions required:
|
||||||
- sanity check
|
- sanity check
|
||||||
- change OBS scene if needed
|
|
||||||
- update display
|
- update display
|
||||||
- update track times
|
- update track times
|
||||||
- update Playdates in database
|
- update Playdates in database
|
||||||
@ -259,9 +278,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check for OBS scene change
|
|
||||||
self.obs_scene_change(row_number)
|
|
||||||
|
|
||||||
# Update Playdates in database
|
# Update Playdates in database
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
Playdates(session, track_sequence.now.track_id)
|
Playdates(session, track_sequence.now.track_id)
|
||||||
@ -580,7 +596,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
count: int = 0
|
count: int = 0
|
||||||
unplayed_count: int = 0
|
|
||||||
duration: int = 0
|
duration: int = 0
|
||||||
|
|
||||||
if prd.note.endswith("+"):
|
if prd.note.endswith("+"):
|
||||||
@ -598,7 +613,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
else:
|
else:
|
||||||
count += 1
|
count += 1
|
||||||
if not row_prd.played:
|
if not row_prd.played:
|
||||||
unplayed_count += 1
|
|
||||||
duration += row_prd.duration
|
duration += row_prd.duration
|
||||||
return (
|
return (
|
||||||
f"{prd.note[:-1].strip()} "
|
f"{prd.note[:-1].strip()} "
|
||||||
@ -636,21 +650,19 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
stripped_note = prd.note[:-1].strip()
|
stripped_note = prd.note[:-1].strip()
|
||||||
if stripped_note:
|
if stripped_note:
|
||||||
return (
|
return (
|
||||||
f"{stripped_note} ["
|
f"{stripped_note} [{count} track{'s' if count > 1 else ''}, "
|
||||||
f"{unplayed_count}/{count} track{'s' if count > 1 else ''} "
|
f"{ms_to_mmss(duration)} unplayed{end_time_str}]"
|
||||||
f"({ms_to_mmss(duration)}) unplayed{end_time_str}]"
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
f"[{unplayed_count}/{count} track{'s' if count > 1 else ''} "
|
f"[Subtotal: {count} track{'s' if count > 1 else ''}, "
|
||||||
f"({ms_to_mmss(duration)}) unplayed{end_time_str}]"
|
f"{ms_to_mmss(duration, none='none')} unplayed{end_time_str}]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
count += 1
|
count += 1
|
||||||
if not row_prd.played:
|
if not row_prd.played:
|
||||||
unplayed_count += 1
|
|
||||||
duration += row_prd.duration
|
duration += row_prd.duration
|
||||||
|
|
||||||
return prd.note
|
return prd.note
|
||||||
@ -670,9 +682,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Return True if row is a header row, else False
|
Return True if row is a header row, else False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if row_number in self.playlist_rows:
|
return self.playlist_rows[row_number].path == ""
|
||||||
return self.playlist_rows[row_number].path == ""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_played_row(self, row_number: int) -> bool:
|
def is_played_row(self, row_number: int) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -713,8 +723,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self.dataChanged.emit(
|
self.dataChanged.emit(
|
||||||
self.index(modified_row, 0),
|
self.index(modified_row, 0), self.index(modified_row, self.columnCount() - 1)
|
||||||
self.index(modified_row, self.columnCount() - 1),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def invalidate_rows(self, modified_rows: List[int]) -> None:
|
def invalidate_rows(self, modified_rows: List[int]) -> None:
|
||||||
@ -725,18 +734,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
for modified_row in modified_rows:
|
for modified_row in modified_rows:
|
||||||
self.invalidate_row(modified_row)
|
self.invalidate_row(modified_row)
|
||||||
|
|
||||||
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
|
||||||
"""
|
|
||||||
If this track_id is in the playlist, return the PlaylistRowData object
|
|
||||||
else return None
|
|
||||||
"""
|
|
||||||
|
|
||||||
for row_number in range(len(self.playlist_rows)):
|
|
||||||
if self.playlist_rows[row_number].track_id == track_id:
|
|
||||||
return self.playlist_rows[row_number]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
"""
|
"""
|
||||||
Mark row as unplayed
|
Mark row as unplayed
|
||||||
@ -872,75 +869,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals.end_reset_model_signal.emit(to_playlist_id)
|
self.signals.end_reset_model_signal.emit(to_playlist_id)
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
|
|
||||||
def move_track_add_note(
|
|
||||||
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Move existing_prd track to new_row_number and append note to any existing note
|
|
||||||
"""
|
|
||||||
|
|
||||||
if note:
|
|
||||||
with Session() as session:
|
|
||||||
plr = session.get(PlaylistRows, existing_prd.plrid)
|
|
||||||
if plr:
|
|
||||||
if plr.note:
|
|
||||||
plr.note += "\n" + note
|
|
||||||
else:
|
|
||||||
plr.note = note
|
|
||||||
|
|
||||||
# Carry out the move outside of the session context to ensure
|
|
||||||
# database updated with any note change
|
|
||||||
self.move_rows([existing_prd.plr_rownum], new_row_number)
|
|
||||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
|
||||||
|
|
||||||
def move_track_to_header(
|
|
||||||
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Add the existing_prd track details to the existing header at header_row_number
|
|
||||||
"""
|
|
||||||
|
|
||||||
if existing_prd.track_id:
|
|
||||||
if note and existing_prd.note:
|
|
||||||
note += "\n" + existing_prd.note
|
|
||||||
self.add_track_to_header(header_row_number, existing_prd.track_id, note)
|
|
||||||
self.delete_rows([existing_prd.plr_rownum])
|
|
||||||
|
|
||||||
def obs_scene_change(self, row_number: int) -> None:
|
|
||||||
"""
|
|
||||||
Check this row and any preceding headers for OBS scene change command
|
|
||||||
and execute any found
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Check any headers before this row
|
|
||||||
idx = row_number - 1
|
|
||||||
while self.is_header_row(idx):
|
|
||||||
idx -= 1
|
|
||||||
# Step through headers in row order and finish with this row
|
|
||||||
for chkrow in range(idx + 1, row_number + 1):
|
|
||||||
match_obj = scene_change_re.search(self.playlist_rows[chkrow].note)
|
|
||||||
if match_obj:
|
|
||||||
scene_name = match_obj.group(1)
|
|
||||||
if scene_name:
|
|
||||||
try:
|
|
||||||
cl = obs.ReqClient(
|
|
||||||
host=Config.OBS_HOST,
|
|
||||||
port=Config.OBS_PORT,
|
|
||||||
password=Config.OBS_PASSWORD,
|
|
||||||
)
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
log.error("OBS connection refused")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
cl.set_current_program_scene(scene_name)
|
|
||||||
log.info(f"OBS scene changed to '{scene_name}'")
|
|
||||||
continue
|
|
||||||
except obs.error.OBSSDKError as e:
|
|
||||||
log.error(f"OBS SDK error ({e})")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
|
||||||
|
|
||||||
def open_in_audacity(self, row_number: int) -> None:
|
def open_in_audacity(self, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Open track at passed row number in Audacity
|
Open track at passed row number in Audacity
|
||||||
@ -974,6 +902,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Update display
|
# Update display
|
||||||
self.invalidate_row(track_sequence.previous.plr_rownum)
|
self.invalidate_row(track_sequence.previous.plr_rownum)
|
||||||
|
|
||||||
|
# Update track times
|
||||||
|
# TODO
|
||||||
|
|
||||||
def refresh_data(self, session: scoped_session):
|
def refresh_data(self, session: scoped_session):
|
||||||
"""Populate dicts for data calls"""
|
"""Populate dicts for data calls"""
|
||||||
|
|
||||||
@ -1012,7 +943,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
set_track_metadata(track)
|
set_track_metadata(track)
|
||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
self.invalidate_row(row_number)
|
self.invalidate_row(row_number)
|
||||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
|
||||||
|
|
||||||
def _reversed_contiguous_row_groups(
|
def _reversed_contiguous_row_groups(
|
||||||
self, row_numbers: List[int]
|
self, row_numbers: List[int]
|
||||||
@ -1408,9 +1338,6 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
def is_played_row(self, row_number: int) -> bool:
|
def is_played_row(self, row_number: int) -> bool:
|
||||||
return self.playlist_model.is_played_row(row_number)
|
return self.playlist_model.is_played_row(row_number)
|
||||||
|
|
||||||
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
|
||||||
return self.playlist_model.is_track_in_playlist(track_id)
|
|
||||||
|
|
||||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
return self.playlist_model.mark_unplayed(row_numbers)
|
return self.playlist_model.mark_unplayed(row_numbers)
|
||||||
|
|
||||||
@ -1424,20 +1351,6 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
from_rows, to_row_number, to_playlist_id
|
from_rows, to_row_number, to_playlist_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def move_track_add_note(
|
|
||||||
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
|
|
||||||
) -> None:
|
|
||||||
return self.playlist_model.move_track_add_note(
|
|
||||||
new_row_number, existing_prd, note
|
|
||||||
)
|
|
||||||
|
|
||||||
def move_track_to_header(
|
|
||||||
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
|
|
||||||
) -> None:
|
|
||||||
return self.playlist_model.move_track_to_header(
|
|
||||||
header_row_number, existing_prd, note
|
|
||||||
)
|
|
||||||
|
|
||||||
def open_in_audacity(self, row_number: int) -> None:
|
def open_in_audacity(self, row_number: int) -> None:
|
||||||
return self.playlist_model.open_in_audacity(row_number)
|
return self.playlist_model.open_in_audacity(row_number)
|
||||||
|
|
||||||
|
|||||||
398
app/playlists.py
398
app/playlists.py
@ -1,3 +1,8 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
import obsws_python as obs # type: ignore
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
@ -6,9 +11,11 @@ from PyQt6.QtCore import (
|
|||||||
QModelIndex,
|
QModelIndex,
|
||||||
QObject,
|
QObject,
|
||||||
QItemSelection,
|
QItemSelection,
|
||||||
|
QItemSelectionModel,
|
||||||
Qt,
|
Qt,
|
||||||
|
# QTimer,
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import QAction, QKeyEvent
|
from PyQt6.QtGui import QAction, QDropEvent, QKeyEvent
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QAbstractItemDelegate,
|
QAbstractItemDelegate,
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
@ -27,21 +34,29 @@ from PyQt6.QtWidgets import (
|
|||||||
QStyleOption,
|
QStyleOption,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dbconfig import Session
|
from dbconfig import Session, scoped_session
|
||||||
from dialogs import TrackSelectDialog
|
from dialogs import TrackSelectDialog
|
||||||
from classes import MusicMusterSignals, track_sequence
|
from classes import MusicMusterSignals, track_sequence
|
||||||
from config import Config
|
from config import Config
|
||||||
from helpers import (
|
from helpers import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
|
file_is_unreadable,
|
||||||
|
get_relative_date,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
|
open_in_audacity,
|
||||||
|
send_mail,
|
||||||
|
set_track_metadata,
|
||||||
show_warning,
|
show_warning,
|
||||||
)
|
)
|
||||||
from models import Settings
|
from log import log
|
||||||
|
from models import PlaylistRows, Settings, Tracks, NoteColours
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from musicmuster import Window
|
from musicmuster import Window
|
||||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||||
|
|
||||||
|
# HEADER_NOTES_COLUMN = 2
|
||||||
|
|
||||||
|
|
||||||
class EscapeDelegate(QStyledItemDelegate):
|
class EscapeDelegate(QStyledItemDelegate):
|
||||||
"""
|
"""
|
||||||
@ -123,7 +138,7 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
else:
|
else:
|
||||||
edit_index = index
|
edit_index = index
|
||||||
|
|
||||||
value = editor.toPlainText().strip()
|
value = editor.toPlainText()
|
||||||
self.playlist_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
self.playlist_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
||||||
|
|
||||||
def updateEditorGeometry(self, editor, option, index):
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
@ -165,6 +180,7 @@ class PlaylistTab(QTableView):
|
|||||||
self.proxy_model = PlaylistProxyModel(self.playlist_model)
|
self.proxy_model = PlaylistProxyModel(self.playlist_model)
|
||||||
self.setItemDelegate(EscapeDelegate(self, self.playlist_model))
|
self.setItemDelegate(EscapeDelegate(self, self.playlist_model))
|
||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
|
# self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
|
||||||
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||||
self.setDragDropOverwriteMode(False)
|
self.setDragDropOverwriteMode(False)
|
||||||
@ -187,12 +203,15 @@ class PlaylistTab(QTableView):
|
|||||||
h_header.setStretchLastSection(True)
|
h_header.setStretchLastSection(True)
|
||||||
# self.signals.set_next_track_signal.connect(self._reset_next)
|
# self.signals.set_next_track_signal.connect(self._reset_next)
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
self.signals.resize_rows_signal.connect(self.resizeRowsToContents)
|
|
||||||
self.signals.span_cells_signal.connect(self._span_cells)
|
self.signals.span_cells_signal.connect(self._span_cells)
|
||||||
|
|
||||||
|
# Call self.eventFilter() for events
|
||||||
|
# self.installEventFilter(self)
|
||||||
|
|
||||||
# Initialise miscellaneous instance variables
|
# Initialise miscellaneous instance variables
|
||||||
self.search_text: str = ""
|
self.search_text: str = ""
|
||||||
self.sort_undo: List[int] = []
|
self.sort_undo: List[int] = []
|
||||||
|
# self.edit_cell_type: Optional[int]
|
||||||
|
|
||||||
# Selection model
|
# Selection model
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
@ -339,7 +358,7 @@ class PlaylistTab(QTableView):
|
|||||||
return self.proxy_model.mapToSource(selected_index).row()
|
return self.proxy_model.mapToSource(selected_index).row()
|
||||||
return selected_index.row()
|
return selected_index.row()
|
||||||
|
|
||||||
def selected_model_row_numbers(self) -> List[int]:
|
def selected_model_row_numbers(self) -> Optional[List[int]]:
|
||||||
"""
|
"""
|
||||||
Return a list of model row numbers corresponding to the selected rows or
|
Return a list of model row numbers corresponding to the selected rows or
|
||||||
an empty list.
|
an empty list.
|
||||||
@ -347,7 +366,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
selected_indexes = self._selected_row_indexes()
|
selected_indexes = self._selected_row_indexes()
|
||||||
if selected_indexes is None:
|
if selected_indexes is None:
|
||||||
return []
|
return None
|
||||||
if hasattr(self.proxy_model, "mapToSource"):
|
if hasattr(self.proxy_model, "mapToSource"):
|
||||||
return [self.proxy_model.mapToSource(a).row() for a in selected_indexes]
|
return [self.proxy_model.mapToSource(a).row() for a in selected_indexes]
|
||||||
return [a.row() for a in selected_indexes]
|
return [a.row() for a in selected_indexes]
|
||||||
@ -388,6 +407,137 @@ class PlaylistTab(QTableView):
|
|||||||
return ""
|
return ""
|
||||||
return self.playlist_model.get_row_track_path(model_row_number)
|
return self.playlist_model.get_row_track_path(model_row_number)
|
||||||
|
|
||||||
|
# def lookup_row_in_songfacts(self) -> None:
|
||||||
|
# """
|
||||||
|
# If there is a selected row and it is a track row,
|
||||||
|
# look up its title in songfacts.
|
||||||
|
|
||||||
|
# If multiple rows are selected, only consider the first one.
|
||||||
|
|
||||||
|
# Otherwise return.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# self._look_up_row(website="songfacts")
|
||||||
|
|
||||||
|
# def lookup_row_in_wikipedia(self) -> None:
|
||||||
|
# """
|
||||||
|
# If there is a selected row and it is a track row,
|
||||||
|
# look up its title in wikipedia.
|
||||||
|
|
||||||
|
# If multiple rows are selected, only consider the first one.
|
||||||
|
|
||||||
|
# Otherwise return.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# self._look_up_row(website="wikipedia")
|
||||||
|
|
||||||
|
# def scroll_current_to_top(self) -> None:
|
||||||
|
# """Scroll currently-playing row to top"""
|
||||||
|
|
||||||
|
# current_row = self._get_current_track_row_number()
|
||||||
|
# if current_row is not None:
|
||||||
|
# self._scroll_to_top(current_row)
|
||||||
|
|
||||||
|
# def scroll_next_to_top(self) -> None:
|
||||||
|
# """Scroll nextly-playing row to top"""
|
||||||
|
|
||||||
|
# next_row = self._get_next_track_row_number()
|
||||||
|
# if next_row is not None:
|
||||||
|
# self._scroll_to_top(next_row)
|
||||||
|
|
||||||
|
def set_search(self, text: str) -> None:
|
||||||
|
"""Set search text and find first match"""
|
||||||
|
|
||||||
|
self.search_text = text
|
||||||
|
if not text:
|
||||||
|
# Search string has been reset
|
||||||
|
return
|
||||||
|
self._search(next=True)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# """
|
||||||
|
# Select next or first row. Don't select section headers.
|
||||||
|
|
||||||
|
# Wrap at last row.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# selected_rows = self._get_selected_rows()
|
||||||
|
# # we will only handle zero or one selected rows
|
||||||
|
# if len(selected_rows) > 1:
|
||||||
|
# return
|
||||||
|
# # select first row if none selected
|
||||||
|
# if len(selected_rows) == 0:
|
||||||
|
# row_number = 0
|
||||||
|
# else:
|
||||||
|
# row_number = selected_rows[0] + 1
|
||||||
|
# if row_number >= self.rowCount():
|
||||||
|
# row_number = 0
|
||||||
|
|
||||||
|
# # Don't select section headers
|
||||||
|
# wrapped = False
|
||||||
|
# track_id = self._get_row_track_id(row_number)
|
||||||
|
# while not track_id:
|
||||||
|
# row_number += 1
|
||||||
|
# if row_number >= self.rowCount():
|
||||||
|
# if wrapped:
|
||||||
|
# # we're already wrapped once, so there are no
|
||||||
|
# # non-headers
|
||||||
|
# return
|
||||||
|
# row_number = 0
|
||||||
|
# wrapped = True
|
||||||
|
# track_id = self._get_row_track_id(row_number)
|
||||||
|
|
||||||
|
# self.selectRow(row_number)
|
||||||
|
|
||||||
|
# def select_previous_row(self) -> None:
|
||||||
|
# """
|
||||||
|
# Select previous or last track. Don't select section headers.
|
||||||
|
# Wrap at first row.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# selected_rows = self._get_selected_rows()
|
||||||
|
# # we will only handle zero or one selected rows
|
||||||
|
# if len(selected_rows) > 1:
|
||||||
|
# return
|
||||||
|
# # select last row if none selected
|
||||||
|
# last_row = self.rowCount() - 1
|
||||||
|
# if len(selected_rows) == 0:
|
||||||
|
# row_number = last_row
|
||||||
|
# else:
|
||||||
|
# row_number = selected_rows[0] - 1
|
||||||
|
# if row_number < 0:
|
||||||
|
# row_number = last_row
|
||||||
|
|
||||||
|
# # Don't select section headers
|
||||||
|
# wrapped = False
|
||||||
|
# track_id = self._get_row_track_id(row_number)
|
||||||
|
# while not track_id:
|
||||||
|
# row_number -= 1
|
||||||
|
# if row_number < 0:
|
||||||
|
# if wrapped:
|
||||||
|
# # we're already wrapped once, so there are no
|
||||||
|
# # non-notes
|
||||||
|
# return
|
||||||
|
# row_number = last_row
|
||||||
|
# wrapped = True
|
||||||
|
# track_id = self._get_row_track_id(row_number)
|
||||||
|
|
||||||
|
# self.selectRow(row_number)
|
||||||
|
|
||||||
def set_row_as_next_track(self) -> None:
|
def set_row_as_next_track(self) -> None:
|
||||||
"""
|
"""
|
||||||
Set selected row as next track
|
Set selected row as next track
|
||||||
@ -412,7 +562,7 @@ class PlaylistTab(QTableView):
|
|||||||
dlg = TrackSelectDialog(
|
dlg = TrackSelectDialog(
|
||||||
session=session,
|
session=session,
|
||||||
new_row_number=model_row_number,
|
new_row_number=model_row_number,
|
||||||
model=self.playlist_model,
|
playlist_id=self.playlist_id,
|
||||||
add_to_header=True,
|
add_to_header=True,
|
||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
@ -510,12 +660,22 @@ class PlaylistTab(QTableView):
|
|||||||
if track_row:
|
if track_row:
|
||||||
self._add_context_menu("Info", lambda: self._info_row(model_row_number))
|
self._add_context_menu("Info", lambda: self._info_row(model_row_number))
|
||||||
|
|
||||||
# Track path
|
# Track path TODO
|
||||||
if track_row:
|
if track_row:
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Copy track path", lambda: self._copy_path(model_row_number)
|
"Copy track path", lambda: self._copy_path(model_row_number)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _calculate_end_time(
|
||||||
|
self, start: Optional[datetime], duration: int
|
||||||
|
) -> Optional[datetime]:
|
||||||
|
"""Return datetime 'duration' ms after 'start'"""
|
||||||
|
|
||||||
|
if start is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return start + timedelta(milliseconds=duration)
|
||||||
|
|
||||||
def _column_resize(self, column_number: int, _old: int, _new: int) -> None:
|
def _column_resize(self, column_number: int, _old: int, _new: int) -> None:
|
||||||
"""
|
"""
|
||||||
Called when column width changes. Save new width to database.
|
Called when column width changes. Save new width to database.
|
||||||
@ -584,7 +744,6 @@ class PlaylistTab(QTableView):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.playlist_model.delete_rows(self.selected_model_row_numbers())
|
self.playlist_model.delete_rows(self.selected_model_row_numbers())
|
||||||
self.clear_selection()
|
|
||||||
|
|
||||||
def get_selected_rows(self) -> List[int]:
|
def get_selected_rows(self) -> List[int]:
|
||||||
"""Return a list of selected row numbers sorted by row"""
|
"""Return a list of selected row numbers sorted by row"""
|
||||||
@ -617,19 +776,133 @@ class PlaylistTab(QTableView):
|
|||||||
info.setDefaultButton(QMessageBox.StandardButton.Cancel)
|
info.setDefaultButton(QMessageBox.StandardButton.Cancel)
|
||||||
info.exec()
|
info.exec()
|
||||||
|
|
||||||
|
def _look_up_row(self, website: str) -> None:
|
||||||
|
"""
|
||||||
|
If there is a selected row and it is a track row,
|
||||||
|
look up its title in the passed website
|
||||||
|
|
||||||
|
If multiple rows are selected, only consider the first one.
|
||||||
|
|
||||||
|
Otherwise return.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("playlists_v3:_look_up_row()")
|
||||||
|
return
|
||||||
|
# selected_row = self._get_selected_row()
|
||||||
|
# if not selected_row:
|
||||||
|
# return
|
||||||
|
|
||||||
|
# if not self._get_row_track_id(selected_row):
|
||||||
|
# return
|
||||||
|
|
||||||
|
# title = self._get_row_title(selected_row)
|
||||||
|
|
||||||
|
# if website == "wikipedia":
|
||||||
|
# QTimer.singleShot(
|
||||||
|
# 0, lambda: self.musicmuster.tabInfolist.open_in_wikipedia(title)
|
||||||
|
# )
|
||||||
|
# elif website == "songfacts":
|
||||||
|
# QTimer.singleShot(
|
||||||
|
# 0, lambda: self.musicmuster.tabInfolist.open_in_songfacts(title)
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# return
|
||||||
|
|
||||||
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.playlist_model.mark_unplayed(row_numbers)
|
self.playlist_model.mark_unplayed(row_numbers)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
|
def _obs_change_scene(self, current_row: int) -> None:
|
||||||
|
"""
|
||||||
|
Try to change OBS scene to the name passed
|
||||||
|
"""
|
||||||
|
|
||||||
|
check_row = current_row
|
||||||
|
while True:
|
||||||
|
# If we have a note and it has a scene change command,
|
||||||
|
# execute it
|
||||||
|
note_text = self._get_row_note(check_row)
|
||||||
|
if note_text:
|
||||||
|
match_obj = scene_change_re.search(note_text)
|
||||||
|
if match_obj:
|
||||||
|
scene_name = match_obj.group(1)
|
||||||
|
if scene_name:
|
||||||
|
try:
|
||||||
|
cl = obs.ReqClient(
|
||||||
|
host=Config.OBS_HOST,
|
||||||
|
port=Config.OBS_PORT,
|
||||||
|
password=Config.OBS_PASSWORD,
|
||||||
|
)
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
log.error("OBS connection refused")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
cl.set_current_program_scene(scene_name)
|
||||||
|
log.info(f"OBS scene changed to '{scene_name}'")
|
||||||
|
return
|
||||||
|
except obs.error.OBSSDKError as e:
|
||||||
|
log.error(f"OBS SDK error ({e})")
|
||||||
|
return
|
||||||
|
# After current track row, only check header rows and stop
|
||||||
|
# at first non-header row
|
||||||
|
check_row -= 1
|
||||||
|
if check_row < 0:
|
||||||
|
break
|
||||||
|
if self._get_row_track_id(check_row):
|
||||||
|
break
|
||||||
|
|
||||||
def _rescan(self, row_number: int) -> None:
|
def _rescan(self, row_number: int) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.playlist_model.rescan_track(row_number)
|
self.playlist_model.rescan_track(row_number)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def scroll_to_top(self, row_number: int) -> None:
|
# def _reset_next(self, old_plrid: int, new_plrid: int) -> None:
|
||||||
|
# """
|
||||||
|
# Called when set_next_track_signal signal received.
|
||||||
|
|
||||||
|
# Actions required:
|
||||||
|
# - If old_plrid points to this playlist:
|
||||||
|
# - Remove existing next track
|
||||||
|
# - If new_plrid points to this playlist:
|
||||||
|
# - Set track as next
|
||||||
|
# - Display row as next track
|
||||||
|
# - Update start/stop times
|
||||||
|
# """
|
||||||
|
|
||||||
|
# with Session() as session:
|
||||||
|
# # Get plrs
|
||||||
|
# old_plr = new_plr = None
|
||||||
|
# if old_plrid:
|
||||||
|
# old_plr = session.get(PlaylistRows, old_plrid)
|
||||||
|
|
||||||
|
# # Unmark next track
|
||||||
|
# if old_plr and old_plr.playlist_id == self.playlist_id:
|
||||||
|
# self._set_row_colour_default(old_plr.plr_rownum)
|
||||||
|
|
||||||
|
# # Mark next track
|
||||||
|
# if new_plrid:
|
||||||
|
# new_plr = session.get(PlaylistRows, new_plrid)
|
||||||
|
# if not new_plr:
|
||||||
|
# log.error(f"_reset_next({new_plrid=}): plr not found")
|
||||||
|
# return
|
||||||
|
# if new_plr.playlist_id == self.playlist_id:
|
||||||
|
# self._set_row_colour_next(new_plr.plr_rownum)
|
||||||
|
|
||||||
|
# # Update start/stop times
|
||||||
|
# self._update_start_end_times(session)
|
||||||
|
|
||||||
|
# self.clear_selection()
|
||||||
|
|
||||||
|
def _run_subprocess(self, args):
|
||||||
|
"""Run args in subprocess"""
|
||||||
|
|
||||||
|
subprocess.call(args)
|
||||||
|
|
||||||
|
def _scroll_to_top(self, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Scroll to put passed row_number Config.SCROLL_TOP_MARGIN from the
|
Scroll to put passed row_number Config.SCROLL_TOP_MARGIN from the
|
||||||
top.
|
top.
|
||||||
@ -638,8 +911,83 @@ class PlaylistTab(QTableView):
|
|||||||
if row_number is None:
|
if row_number is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
row_index = self.proxy_model.index(row_number, 0)
|
padding_required = Config.SCROLL_TOP_MARGIN
|
||||||
self.scrollTo(row_index, QAbstractItemView.ScrollHint.PositionAtTop)
|
top_row = row_number
|
||||||
|
|
||||||
|
if row_number > Config.SCROLL_TOP_MARGIN:
|
||||||
|
# We can't scroll to a hidden row. Calculate target_row as
|
||||||
|
# the one that is ideal to be at the top. Then count upwards
|
||||||
|
# from passed row_number until we either reach the target,
|
||||||
|
# pass it or reach row_number 0.
|
||||||
|
for i in range(row_number - 1, -1, -1):
|
||||||
|
if self.isRowHidden(i):
|
||||||
|
continue
|
||||||
|
if padding_required == 0:
|
||||||
|
break
|
||||||
|
top_row = i
|
||||||
|
padding_required -= 1
|
||||||
|
|
||||||
|
scroll_item = self.item(top_row, 0)
|
||||||
|
self.scrollToItem(scroll_item, QAbstractItemView.ScrollHint.PositionAtTop)
|
||||||
|
|
||||||
|
# def _search(self, next: bool = True) -> None:
|
||||||
|
# """
|
||||||
|
# Select next/previous row containg self.search_string. Start from
|
||||||
|
# top selected row if there is one, else from top.
|
||||||
|
|
||||||
|
# Wrap at last/first row.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# if not self.search_text:
|
||||||
|
# return
|
||||||
|
|
||||||
|
# selected_row = self._get_selected_row()
|
||||||
|
# if next:
|
||||||
|
# if selected_row is not None and selected_row < self.rowCount() - 1:
|
||||||
|
# starting_row = selected_row + 1
|
||||||
|
# else:
|
||||||
|
# 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
|
||||||
|
# match_row = None
|
||||||
|
# row_number = starting_row
|
||||||
|
# needle = self.search_text.lower()
|
||||||
|
# while True:
|
||||||
|
# # Check for match in title, artist or notes
|
||||||
|
# title = self._get_row_title(row_number)
|
||||||
|
# if title and needle in title.lower():
|
||||||
|
# match_row = row_number
|
||||||
|
# break
|
||||||
|
# artist = self._get_row_artist(row_number)
|
||||||
|
# if artist and needle in artist.lower():
|
||||||
|
# match_row = row_number
|
||||||
|
# break
|
||||||
|
# note = self._get_row_note(row_number)
|
||||||
|
# if note and needle in note.lower():
|
||||||
|
# match_row = row_number
|
||||||
|
# break
|
||||||
|
# if next:
|
||||||
|
# row_number += 1
|
||||||
|
# if wrapped and row_number >= starting_row:
|
||||||
|
# break
|
||||||
|
# if row_number >= self.rowCount():
|
||||||
|
# row_number = 0
|
||||||
|
# wrapped = True
|
||||||
|
# else:
|
||||||
|
# row_number -= 1
|
||||||
|
# if wrapped and row_number <= starting_row:
|
||||||
|
# break
|
||||||
|
# if row_number < 0:
|
||||||
|
# row_number = self.rowCount() - 1
|
||||||
|
# wrapped = True
|
||||||
|
|
||||||
|
# if match_row is not None:
|
||||||
|
# self.selectRow(row_number)
|
||||||
|
|
||||||
def select_duplicate_rows(self) -> None:
|
def select_duplicate_rows(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -704,6 +1052,30 @@ class PlaylistTab(QTableView):
|
|||||||
else:
|
else:
|
||||||
self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH)
|
self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH)
|
||||||
|
|
||||||
|
# def _set_row_note_colour(self, session: scoped_session, row_number: int) -> None:
|
||||||
|
# """
|
||||||
|
# Set row note colour
|
||||||
|
# """
|
||||||
|
|
||||||
|
# # Sanity check: this should be a track row and thus have a
|
||||||
|
# # track associated
|
||||||
|
# if not self._get_row_track_id(row_number):
|
||||||
|
# if os.environ["MM_ENV"] == "PRODUCTION":
|
||||||
|
# send_mail(
|
||||||
|
# Config.ERRORS_TO,
|
||||||
|
# Config.ERRORS_FROM,
|
||||||
|
# "playlists:_set_row_note_colour() on header row",
|
||||||
|
# stackprinter.format(),
|
||||||
|
# )
|
||||||
|
# # stackprinter.show(add_summary=True, style="darkbg")
|
||||||
|
# print(f"playists:_set_row_note_colour() called on track row ({row_number=}")
|
||||||
|
# return
|
||||||
|
|
||||||
|
# # Set colour
|
||||||
|
# note_text = self._get_row_note(row_number)
|
||||||
|
# note_colour = NoteColours.get_colour(session, note_text)
|
||||||
|
# self._set_cell_colour(row_number, ROW_NOTES, note_colour)
|
||||||
|
|
||||||
def _span_cells(self, row: int, column: int, rowSpan: int, columnSpan: int) -> None:
|
def _span_cells(self, row: int, column: int, rowSpan: int, columnSpan: int) -> None:
|
||||||
"""
|
"""
|
||||||
Implement spanning of cells, initiated by signal
|
Implement spanning of cells, initiated by signal
|
||||||
|
|||||||
@ -786,6 +786,9 @@ padding-left: 8px;</string>
|
|||||||
</property>
|
</property>
|
||||||
<addaction name="actionSearch"/>
|
<addaction name="actionSearch"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionSelect_next_track"/>
|
||||||
|
<addaction name="actionSelect_previous_track"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSearch_title_in_Wikipedia"/>
|
<addaction name="actionSearch_title_in_Wikipedia"/>
|
||||||
<addaction name="actionSearch_title_in_Songfacts"/>
|
<addaction name="actionSearch_title_in_Songfacts"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
|||||||
@ -493,6 +493,9 @@ class Ui_MainWindow(object):
|
|||||||
self.menuPlaylist.addAction(self.actionPaste)
|
self.menuPlaylist.addAction(self.actionPaste)
|
||||||
self.menuSearc_h.addAction(self.actionSearch)
|
self.menuSearc_h.addAction(self.actionSearch)
|
||||||
self.menuSearc_h.addSeparator()
|
self.menuSearc_h.addSeparator()
|
||||||
|
self.menuSearc_h.addAction(self.actionSelect_next_track)
|
||||||
|
self.menuSearc_h.addAction(self.actionSelect_previous_track)
|
||||||
|
self.menuSearc_h.addSeparator()
|
||||||
self.menuSearc_h.addAction(self.actionSearch_title_in_Wikipedia)
|
self.menuSearc_h.addAction(self.actionSearch_title_in_Wikipedia)
|
||||||
self.menuSearc_h.addAction(self.actionSearch_title_in_Songfacts)
|
self.menuSearc_h.addAction(self.actionSearch_title_in_Songfacts)
|
||||||
self.menuHelp.addAction(self.action_About)
|
self.menuHelp.addAction(self.action_About)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user