Compare commits
13 Commits
60c085ad12
...
e43c9f3b17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e43c9f3b17 | ||
|
|
2bf1e442be | ||
|
|
4b6c8b0634 | ||
|
|
2432039b72 | ||
|
|
74bdbe2975 | ||
|
|
f228a371f2 | ||
|
|
b74007119d | ||
|
|
45243759b8 | ||
|
|
d73bdb264d | ||
|
|
90f8e20843 | ||
|
|
184318078f | ||
|
|
c6befd219c | ||
|
|
2f0ad5cd52 |
@ -58,8 +58,8 @@ class Config(object):
|
|||||||
HIDE_AFTER_PLAYING_OFFSET = 5000
|
HIDE_AFTER_PLAYING_OFFSET = 5000
|
||||||
INFO_TAB_TITLE_LENGTH = 15
|
INFO_TAB_TITLE_LENGTH = 15
|
||||||
LAST_PLAYED_TODAY_STRING = "Today"
|
LAST_PLAYED_TODAY_STRING = "Today"
|
||||||
LOG_LEVEL_STDERR = logging.ERROR
|
LOG_LEVEL_STDERR = logging.INFO
|
||||||
LOG_LEVEL_SYSLOG = logging.DEBUG
|
LOG_LEVEL_SYSLOG = logging.INFO
|
||||||
LOG_NAME = "musicmuster"
|
LOG_NAME = "musicmuster"
|
||||||
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
|
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
|
||||||
MAIL_PORT = int(os.environ.get("MAIL_PORT") or 25)
|
MAIL_PORT = int(os.environ.get("MAIL_PORT") or 25)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from helpers import (
|
|||||||
get_relative_date,
|
get_relative_date,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
)
|
)
|
||||||
|
from log import log
|
||||||
from models import Settings, Tracks
|
from models import Settings, Tracks
|
||||||
from playlistmodel import PlaylistModel
|
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 +23,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: scoped_session,
|
||||||
new_row_number: int,
|
new_row_number: int,
|
||||||
model: PlaylistModel,
|
source_model: PlaylistModel,
|
||||||
add_to_header: Optional[bool] = False,
|
add_to_header: Optional[bool] = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -34,7 +35,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.source_model = source_model
|
||||||
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)
|
||||||
@ -69,25 +70,30 @@ class TrackSelectDialog(QDialog):
|
|||||||
track = item.data(Qt.ItemDataRole.UserRole)
|
track = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
|
||||||
note = self.ui.txtNote.text()
|
note = self.ui.txtNote.text()
|
||||||
|
|
||||||
|
if not (track or note):
|
||||||
|
return
|
||||||
|
|
||||||
track_id = None
|
track_id = None
|
||||||
if track:
|
if track:
|
||||||
track_id = track.id
|
track_id = track.id
|
||||||
|
|
||||||
if not track_id:
|
if note and not track_id:
|
||||||
if note:
|
self.source_model.insert_row(self.new_row_number, track_id, note)
|
||||||
self.model.insert_row(self.new_row_number, track_id, note)
|
|
||||||
self.ui.txtNote.clear()
|
self.ui.txtNote.clear()
|
||||||
return
|
self.new_row_number += 1
|
||||||
else:
|
|
||||||
# No note, no track
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.ui.txtNote.clear()
|
self.ui.txtNote.clear()
|
||||||
self.select_searchtext()
|
self.select_searchtext()
|
||||||
|
|
||||||
|
if track_id is None:
|
||||||
|
log.error("track_id is None and should not be")
|
||||||
|
return
|
||||||
|
|
||||||
# Check whether track is already in playlist
|
# Check whether track is already in playlist
|
||||||
move_existing = False
|
move_existing = False
|
||||||
existing_prd = self.model.is_track_in_playlist(track_id)
|
existing_prd = self.source_model.is_track_in_playlist(track_id)
|
||||||
if existing_prd is not None:
|
if existing_prd is not None:
|
||||||
if ask_yes_no(
|
if ask_yes_no(
|
||||||
"Duplicate row",
|
"Duplicate row",
|
||||||
@ -98,17 +104,19 @@ class TrackSelectDialog(QDialog):
|
|||||||
|
|
||||||
if self.add_to_header:
|
if self.add_to_header:
|
||||||
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
||||||
self.model.move_track_to_header(self.new_row_number, existing_prd, note)
|
self.source_model.move_track_to_header(self.new_row_number, existing_prd, note)
|
||||||
else:
|
else:
|
||||||
self.model.add_track_to_header(self.new_row_number, track_id)
|
self.source_model.add_track_to_header(self.new_row_number, track_id)
|
||||||
# Close dialog - we can only add one track to a header
|
# Close dialog - we can only add one track to a header
|
||||||
self.accept()
|
self.accept()
|
||||||
else:
|
else:
|
||||||
# Adding a new track row
|
# Adding a new track row
|
||||||
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
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)
|
self.source_model.move_track_add_note(self.new_row_number, existing_prd, note)
|
||||||
else:
|
else:
|
||||||
self.model.insert_row(self.new_row_number, track_id, note)
|
self.source_model.insert_row(self.new_row_number, track_id, note)
|
||||||
|
|
||||||
|
self.new_row_number += 1
|
||||||
|
|
||||||
def add_selected_and_close(self) -> None:
|
def add_selected_and_close(self) -> None:
|
||||||
"""Handle Add and Close button"""
|
"""Handle Add and Close button"""
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from PyQt6.QtWidgets import QTabWidget
|
|||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
from classes import MusicMusterSignals
|
from classes import MusicMusterSignals
|
||||||
|
from log import log
|
||||||
|
|
||||||
|
|
||||||
class InfoTabs(QTabWidget):
|
class InfoTabs(QTabWidget):
|
||||||
@ -37,6 +38,7 @@ class InfoTabs(QTabWidget):
|
|||||||
"""Search Songfacts for title"""
|
"""Search Songfacts for title"""
|
||||||
|
|
||||||
slug = slugify(title, replacements=([["'", ""]]))
|
slug = slugify(title, replacements=([["'", ""]]))
|
||||||
|
log.info(f"Songfacts Infotab for {title=}")
|
||||||
url = f"https://www.songfacts.com/search/songs/{slug}"
|
url = f"https://www.songfacts.com/search/songs/{slug}"
|
||||||
|
|
||||||
self.open_tab(url, title)
|
self.open_tab(url, title)
|
||||||
@ -45,6 +47,7 @@ class InfoTabs(QTabWidget):
|
|||||||
"""Search Wikipedia for title"""
|
"""Search Wikipedia for title"""
|
||||||
|
|
||||||
str = urllib.parse.quote_plus(title)
|
str = urllib.parse.quote_plus(title)
|
||||||
|
log.info(f"Wikipedia Infotab for {title=}")
|
||||||
url = f"https://www.wikipedia.org/w/index.php?search={str}"
|
url = f"https://www.wikipedia.org/w/index.php?search={str}"
|
||||||
|
|
||||||
self.open_tab(url, title)
|
self.open_tab(url, title)
|
||||||
|
|||||||
42
app/log.py
42
app/log.py
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import colorlog
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
@ -22,50 +23,29 @@ class LevelTagFilter(logging.Filter):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DebugStdoutFilter(logging.Filter):
|
|
||||||
"""Filter debug messages sent to stdout"""
|
|
||||||
|
|
||||||
def filter(self, record: logging.LogRecord):
|
|
||||||
# Exceptions are logged at ERROR level
|
|
||||||
if record.levelno in [logging.DEBUG, logging.ERROR]:
|
|
||||||
return True
|
|
||||||
if record.module in Config.DEBUG_MODULES:
|
|
||||||
return True
|
|
||||||
if record.funcName in Config.DEBUG_FUNCTIONS:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(Config.LOG_NAME)
|
log = logging.getLogger(Config.LOG_NAME)
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
|
local_filter = LevelTagFilter()
|
||||||
|
|
||||||
# stderr
|
# stderr
|
||||||
stderr = logging.StreamHandler()
|
stderr = colorlog.StreamHandler()
|
||||||
stderr.setLevel(Config.LOG_LEVEL_STDERR)
|
stderr.setLevel(Config.LOG_LEVEL_STDERR)
|
||||||
|
stderr.addFilter(local_filter)
|
||||||
|
stderr_fmt = colorlog.ColoredFormatter(
|
||||||
|
"%(log_color)s[%(asctime)s] %(filename)s:%(lineno)s %(leveltag)s:%(message)s",
|
||||||
|
datefmt="%H:%M:%S"
|
||||||
|
)
|
||||||
|
stderr.setFormatter(stderr_fmt)
|
||||||
|
log.addHandler(stderr)
|
||||||
|
|
||||||
# syslog
|
# syslog
|
||||||
syslog = logging.handlers.SysLogHandler(address="/dev/log")
|
syslog = logging.handlers.SysLogHandler(address="/dev/log")
|
||||||
syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
|
syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
|
||||||
|
|
||||||
# Filter
|
|
||||||
local_filter = LevelTagFilter()
|
|
||||||
debug_filter = DebugStdoutFilter()
|
|
||||||
|
|
||||||
syslog.addFilter(local_filter)
|
syslog.addFilter(local_filter)
|
||||||
|
|
||||||
stderr.addFilter(local_filter)
|
|
||||||
stderr.addFilter(debug_filter)
|
|
||||||
|
|
||||||
stderr_fmt = logging.Formatter(
|
|
||||||
"[%(asctime)s] %(leveltag)s: %(message)s", datefmt="%H:%M:%S"
|
|
||||||
)
|
|
||||||
syslog_fmt = logging.Formatter(
|
syslog_fmt = logging.Formatter(
|
||||||
"[%(name)s] %(module)s.%(funcName)s - %(leveltag)s: %(message)s"
|
"[%(name)s] %(filename)s:%(lineno)s %(leveltag)s: %(message)s"
|
||||||
)
|
)
|
||||||
stderr.setFormatter(stderr_fmt)
|
|
||||||
syslog.setFormatter(syslog_fmt)
|
syslog.setFormatter(syslog_fmt)
|
||||||
|
|
||||||
log.addHandler(stderr)
|
|
||||||
log.addHandler(syslog)
|
log.addHandler(syslog)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -66,6 +66,8 @@ class Music:
|
|||||||
to hold up the UI during the fade.
|
to hold up the UI during the fade.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info("Music.stop()")
|
||||||
|
|
||||||
if not self.player:
|
if not self.player:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -96,6 +98,8 @@ class Music:
|
|||||||
Log and return if path not found.
|
Log and return if path not found.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"Music.play({path=}, {position=}")
|
||||||
|
|
||||||
if file_is_unreadable(path):
|
if file_is_unreadable(path):
|
||||||
log.error(f"play({path}): path not readable")
|
log.error(f"play({path}): path not readable")
|
||||||
return None
|
return None
|
||||||
@ -125,6 +129,8 @@ class Music:
|
|||||||
def stop(self) -> float:
|
def stop(self) -> float:
|
||||||
"""Immediately stop playing"""
|
"""Immediately stop playing"""
|
||||||
|
|
||||||
|
log.info("Music.stop()")
|
||||||
|
|
||||||
if not self.player:
|
if not self.player:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
|||||||
@ -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], source_model: PlaylistModel, row_number: Optional[int]
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.filenames = filenames
|
self.filenames = filenames
|
||||||
self.model = model
|
self.source_model = source_model
|
||||||
self.next_row_number = row_number
|
self.next_row_number = row_number
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ 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.source_model.insert_row(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
|
||||||
@ -216,11 +216,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
FadeCurve.GraphWidget = self.widgetFadeVolume
|
FadeCurve.GraphWidget = self.widgetFadeVolume
|
||||||
|
|
||||||
self.active_tab = lambda: self.tabPlaylist.currentWidget()
|
self.active_tab = lambda: self.tabPlaylist.currentWidget()
|
||||||
self.active_model = lambda: self.tabPlaylist.currentWidget().model()
|
self.active_proxy_model = lambda: self.tabPlaylist.currentWidget().model()
|
||||||
self.move_source_rows: Optional[List[int]] = None
|
self.move_source_rows: Optional[List[int]] = None
|
||||||
self.move_source_model: Optional[PlaylistProxyModel] = None
|
self.move_source_model: Optional[PlaylistProxyModel] = None
|
||||||
|
self.audacity_file_path: Optional[str] = None
|
||||||
|
|
||||||
self.load_last_playlists()
|
|
||||||
if Config.CARTS_HIDE:
|
if Config.CARTS_HIDE:
|
||||||
self.cartsWidget.hide()
|
self.cartsWidget.hide()
|
||||||
self.frame_6.hide()
|
self.frame_6.hide()
|
||||||
@ -233,6 +233,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.timer1000.start(1000)
|
self.timer1000.start(1000)
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
self.connect_signals_slots()
|
self.connect_signals_slots()
|
||||||
|
self.load_last_playlists()
|
||||||
|
|
||||||
def about(self) -> None:
|
def about(self) -> None:
|
||||||
"""Get git tag and database name"""
|
"""Get git tag and database name"""
|
||||||
@ -541,6 +542,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.btnStop.clicked.connect(self.stop)
|
self.btnStop.clicked.connect(self.stop)
|
||||||
self.hdrCurrentTrack.clicked.connect(self.show_current)
|
self.hdrCurrentTrack.clicked.connect(self.show_current)
|
||||||
self.hdrNextTrack.clicked.connect(self.show_next)
|
self.hdrNextTrack.clicked.connect(self.show_next)
|
||||||
|
self.tabPlaylist.currentChanged.connect(self.tab_change)
|
||||||
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
|
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
|
||||||
self.tabBar = self.tabPlaylist.tabBar()
|
self.tabBar = self.tabPlaylist.tabBar()
|
||||||
self.txtSearch.textChanged.connect(self.search_playlist_text_changed)
|
self.txtSearch.textChanged.connect(self.search_playlist_text_changed)
|
||||||
@ -559,6 +561,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
) -> Optional[Playlists]:
|
) -> Optional[Playlists]:
|
||||||
"""Create new playlist"""
|
"""Create new playlist"""
|
||||||
|
|
||||||
|
log.info(f"create_playlist({playlist_name=}")
|
||||||
|
|
||||||
playlist_name = self.solicit_playlist_name(session)
|
playlist_name = self.solicit_playlist_name(session)
|
||||||
if not playlist_name:
|
if not playlist_name:
|
||||||
return None
|
return None
|
||||||
@ -567,6 +571,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if playlist:
|
if playlist:
|
||||||
playlist.mark_open()
|
playlist.mark_open()
|
||||||
return playlist
|
return playlist
|
||||||
|
else:
|
||||||
|
log.error("Failed to create playlist")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -584,15 +590,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
add tab to display. Return index number of tab.
|
add tab to display. Return index number of tab.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert playlist.id
|
log.info(f"create_playlist_tab({playlist=})")
|
||||||
|
|
||||||
playlist_tab = PlaylistTab(
|
playlist_tab = PlaylistTab(
|
||||||
musicmuster=self,
|
musicmuster=self,
|
||||||
playlist_id=playlist.id,
|
playlist_id=playlist.id,
|
||||||
)
|
)
|
||||||
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
|
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||||
self.tabPlaylist.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
|
log.info(f"create_playlist_tab() returned: {idx=}")
|
||||||
return idx
|
return idx
|
||||||
|
|
||||||
def cut_rows(self) -> None:
|
def cut_rows(self) -> None:
|
||||||
@ -603,7 +609,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# Save the selected PlaylistRows items ready for a later
|
# Save the selected PlaylistRows items ready for a later
|
||||||
# paste
|
# paste
|
||||||
self.move_source_rows = self.active_tab().get_selected_rows()
|
self.move_source_rows = self.active_tab().get_selected_rows()
|
||||||
self.move_source_model = self.active_model()
|
self.move_source_model = self.active_proxy_model()
|
||||||
|
|
||||||
|
log.info(f"cut_rows(): {self.move_source_rows=} {self.move_source_model=}")
|
||||||
|
|
||||||
def debug(self):
|
def debug(self):
|
||||||
"""Invoke debugger"""
|
"""Invoke debugger"""
|
||||||
@ -629,6 +637,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
):
|
):
|
||||||
if self.close_playlist_tab():
|
if self.close_playlist_tab():
|
||||||
playlist.delete(session)
|
playlist.delete(session)
|
||||||
|
else:
|
||||||
|
log.error("Failed to retrieve playlist")
|
||||||
|
|
||||||
def disable_play_next_controls(self) -> None:
|
def disable_play_next_controls(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -679,6 +689,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
so we need to disable it here while editing.
|
so we need to disable it here while editing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"enable_escape({enabled=})")
|
||||||
|
|
||||||
self.action_Clear_selection.setEnabled(enabled)
|
self.action_Clear_selection.setEnabled(enabled)
|
||||||
|
|
||||||
def enable_play_next_controls(self) -> None:
|
def enable_play_next_controls(self) -> None:
|
||||||
@ -758,11 +770,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
if self.hide_played_tracks:
|
if self.hide_played_tracks:
|
||||||
self.hide_played_tracks = False
|
self.hide_played_tracks = False
|
||||||
self.active_model().hide_played_tracks(False)
|
self.active_proxy_model().hide_played_tracks(False)
|
||||||
self.btnHidePlayed.setText("Hide played")
|
self.btnHidePlayed.setText("Hide played")
|
||||||
else:
|
else:
|
||||||
self.hide_played_tracks = True
|
self.hide_played_tracks = True
|
||||||
self.active_model().hide_played_tracks(True)
|
self.active_proxy_model().hide_played_tracks(True)
|
||||||
self.btnHidePlayed.setText("Show played")
|
self.btnHidePlayed.setText("Show played")
|
||||||
|
|
||||||
def import_track(self) -> None:
|
def import_track(self) -> None:
|
||||||
@ -828,8 +840,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_proxy_model(),
|
||||||
self.active_tab().selected_model_row_number(),
|
self.active_tab().source_model_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)
|
||||||
@ -841,12 +853,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def insert_header(self) -> None:
|
def insert_header(self) -> None:
|
||||||
"""Show dialog box to enter header text and add to playlist"""
|
"""Show dialog box to enter header text and add to playlist"""
|
||||||
|
|
||||||
try:
|
proxy_model = self.active_proxy_model()
|
||||||
model = cast(PlaylistModel, self.active_tab().model())
|
if proxy_model is None:
|
||||||
if model is None:
|
log.error("No proxy model")
|
||||||
return
|
|
||||||
except AttributeError:
|
|
||||||
# Just return if there's no visible playlist tab model
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get header text
|
# Get header text
|
||||||
@ -856,8 +865,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
dlg.resize(500, 100)
|
dlg.resize(500, 100)
|
||||||
ok = dlg.exec()
|
ok = dlg.exec()
|
||||||
if ok:
|
if ok:
|
||||||
model.insert_row(
|
proxy_model.insert_row(
|
||||||
proposed_row_number=self.active_tab().selected_model_row_number(),
|
proposed_row_number=self.active_tab().source_model_selected_row_number(),
|
||||||
note=dlg.textValue(),
|
note=dlg.textValue(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -867,8 +876,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().source_model_selected_row_number(),
|
||||||
model=self.active_model(),
|
source_model=self.active_proxy_model(),
|
||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
@ -881,6 +890,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if playlist:
|
if playlist:
|
||||||
_ = self.create_playlist_tab(playlist)
|
_ = self.create_playlist_tab(playlist)
|
||||||
playlist_ids.append(playlist.id)
|
playlist_ids.append(playlist.id)
|
||||||
|
log.info(f"load_last_playlists() loaded {playlist=}")
|
||||||
# Set active tab
|
# Set active tab
|
||||||
record = Settings.get_int_settings(session, "active_tab")
|
record = Settings.get_int_settings(session, "active_tab")
|
||||||
if record.f_int is not None and record.f_int >= 0:
|
if record.f_int is not None and record.f_int >= 0:
|
||||||
@ -897,11 +907,11 @@ 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().source_model_selected_row_number()
|
||||||
if row_number is None:
|
if row_number is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
track_info = self.active_model().get_row_info(row_number)
|
track_info = self.active_proxy_model().get_row_info(row_number)
|
||||||
if track_info is None:
|
if track_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -912,11 +922,11 @@ 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().source_model_selected_row_number()
|
||||||
if row_number is None:
|
if row_number is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
track_info = self.active_model().get_row_info(row_number)
|
track_info = self.active_proxy_model().get_row_info(row_number)
|
||||||
if track_info is None:
|
if track_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -953,7 +963,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
to_row = 0
|
to_row = 0
|
||||||
|
|
||||||
# Move rows
|
# Move rows
|
||||||
self.active_model().move_rows_between_playlists(
|
self.active_proxy_model().move_rows_between_playlists(
|
||||||
row_numbers, to_row, to_playlist_id
|
row_numbers, to_row, to_playlist_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -973,7 +983,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Move unplayed rows to another playlist
|
Move unplayed rows to another playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
unplayed_rows = self.active_model().get_unplayed_rows()
|
unplayed_rows = self.active_proxy_model().get_unplayed_rows()
|
||||||
if not unplayed_rows:
|
if not unplayed_rows:
|
||||||
return
|
return
|
||||||
self.move_playlist_rows(unplayed_rows)
|
self.move_playlist_rows(unplayed_rows)
|
||||||
@ -989,6 +999,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if template:
|
if template:
|
||||||
playlist_name = self.solicit_playlist_name(session)
|
playlist_name = self.solicit_playlist_name(session)
|
||||||
if not playlist_name:
|
if not playlist_name:
|
||||||
|
log.error("Template has no name")
|
||||||
return
|
return
|
||||||
playlist = Playlists.create_playlist_from_template(
|
playlist = Playlists.create_playlist_from_template(
|
||||||
session, template, playlist_name
|
session, template, playlist_name
|
||||||
@ -999,6 +1010,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
if playlist:
|
if playlist:
|
||||||
|
log.error("Playlist failed to create")
|
||||||
playlist.mark_open()
|
playlist.mark_open()
|
||||||
self.create_playlist_tab(playlist)
|
self.create_playlist_tab(playlist)
|
||||||
|
|
||||||
@ -1011,9 +1023,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
dlg.exec()
|
dlg.exec()
|
||||||
playlist = dlg.playlist
|
playlist = dlg.playlist
|
||||||
if playlist:
|
if playlist:
|
||||||
self.create_playlist_tab(playlist)
|
idx = self.create_playlist_tab(playlist)
|
||||||
playlist.mark_open()
|
playlist.mark_open()
|
||||||
|
|
||||||
|
self.tabPlaylist.setCurrentIndex(idx)
|
||||||
|
|
||||||
def paste_rows(self) -> None:
|
def paste_rows(self) -> None:
|
||||||
"""
|
"""
|
||||||
Paste earlier cut rows.
|
Paste earlier cut rows.
|
||||||
@ -1022,18 +1036,21 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if self.move_source_rows is None or self.move_source_model is None:
|
if self.move_source_rows is None or self.move_source_model is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
to_playlist_id = self.active_tab().playlist_id
|
to_playlist_model = self.active_tab().source_model
|
||||||
selected_rows = self.active_tab().get_selected_rows()
|
selected_rows = self.active_tab().get_selected_rows()
|
||||||
if selected_rows:
|
if selected_rows:
|
||||||
destination_row = selected_rows[0]
|
destination_row = selected_rows[0]
|
||||||
else:
|
else:
|
||||||
destination_row = self.active_model().rowCount()
|
destination_row = self.active_proxy_model().rowCount()
|
||||||
|
|
||||||
if to_playlist_id == self.move_source_model.data_model.playlist_id:
|
if (
|
||||||
|
to_playlist_model.playlist_id
|
||||||
|
== self.move_source_model.source_model.playlist_id
|
||||||
|
):
|
||||||
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
||||||
else:
|
else:
|
||||||
self.move_source_model.move_rows_between_playlists(
|
self.move_source_model.move_rows_between_playlists(
|
||||||
self.move_source_rows, destination_row, to_playlist_id
|
self.move_source_rows, destination_row, to_playlist_model
|
||||||
)
|
)
|
||||||
self.move_source_rows = self.move_source_model = None
|
self.move_source_rows = self.move_source_model = None
|
||||||
|
|
||||||
@ -1056,12 +1073,14 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
- Update headers
|
- Update headers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"play_next({position=})")
|
||||||
|
|
||||||
# If there is no next track set, return.
|
# If there is no next track set, return.
|
||||||
if not track_sequence.next.track_id:
|
if not track_sequence.next.track_id:
|
||||||
log.debug("musicmuster.play_next(): no next track selected")
|
log.error("musicmuster.play_next(): no next track selected")
|
||||||
return
|
return
|
||||||
if not track_sequence.next.path:
|
if not track_sequence.next.path:
|
||||||
log.debug("musicmuster.play_next(): no path for next track")
|
log.error("musicmuster.play_next(): no path for next track")
|
||||||
return
|
return
|
||||||
|
|
||||||
# If there's currently a track playing, fade it.
|
# If there's currently a track playing, fade it.
|
||||||
@ -1082,9 +1101,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# Show closing volume graph
|
# Show closing volume graph
|
||||||
if track_sequence.now.fade_graph:
|
if track_sequence.now.fade_graph:
|
||||||
track_sequence.now.fade_graph.plot()
|
track_sequence.now.fade_graph.plot()
|
||||||
|
else:
|
||||||
|
log.error("No fade_graph")
|
||||||
|
|
||||||
# Play (new) current track
|
# Play (new) current track
|
||||||
if not track_sequence.now.path:
|
if not track_sequence.now.path:
|
||||||
|
log.error("No path for next track")
|
||||||
return
|
return
|
||||||
track_sequence.now.start()
|
track_sequence.now.start()
|
||||||
self.music.play(track_sequence.now.path, position)
|
self.music.play(track_sequence.now.path, position)
|
||||||
@ -1099,12 +1121,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
volume = self.music.player.audio_get_volume()
|
volume = self.music.player.audio_get_volume()
|
||||||
if volume < Config.VOLUME_VLC_DEFAULT:
|
if volume < Config.VOLUME_VLC_DEFAULT:
|
||||||
self.music.set_volume()
|
self.music.set_volume()
|
||||||
log.error(f"Reset from {volume=}")
|
log.warn(f"Reset from {volume=}")
|
||||||
break
|
break
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
|
|
||||||
# Notify model
|
# Notify model
|
||||||
self.active_model().current_track_started()
|
self.active_proxy_model().current_track_started()
|
||||||
|
|
||||||
# Note that track is now playing
|
# Note that track is now playing
|
||||||
self.playing = True
|
self.playing = True
|
||||||
@ -1163,8 +1185,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
- If a track is playing, make that the next track
|
- If a track is playing, make that the next track
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info("resume()")
|
||||||
|
|
||||||
# Return if no saved position
|
# Return if no saved position
|
||||||
if not track_sequence.previous.resume_marker:
|
if not track_sequence.previous.resume_marker:
|
||||||
|
log.error("No previous track position")
|
||||||
return
|
return
|
||||||
|
|
||||||
# We want to use play_next() to resume, so copy the previous
|
# We want to use play_next() to resume, so copy the previous
|
||||||
@ -1235,7 +1260,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Incremental search of playlist
|
Incremental search of playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.active_model().set_incremental_search(self.txtSearch.text())
|
self.active_proxy_model().set_incremental_search(self.txtSearch.text())
|
||||||
|
|
||||||
def select_next_row(self) -> None:
|
def select_next_row(self) -> None:
|
||||||
"""Select next or first row in playlist"""
|
"""Select next or first row in playlist"""
|
||||||
@ -1276,6 +1301,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
playlist_tab = self.active_tab()
|
playlist_tab = self.active_tab()
|
||||||
if playlist_tab:
|
if playlist_tab:
|
||||||
playlist_tab.set_row_as_next_track()
|
playlist_tab.set_row_as_next_track()
|
||||||
|
else:
|
||||||
|
log.error("No active tab")
|
||||||
|
|
||||||
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
|
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1325,7 +1352,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.tabPlaylist.setCurrentIndex(idx)
|
self.tabPlaylist.setCurrentIndex(idx)
|
||||||
break
|
break
|
||||||
|
|
||||||
self.tabPlaylist.currentWidget().scroll_to_top(plt.plr_rownum)
|
display_row = self.active_proxy_model().mapFromSource(
|
||||||
|
self.active_proxy_model().source_model.index(plt.plr_rownum, 0)
|
||||||
|
).row()
|
||||||
|
self.tabPlaylist.currentWidget().scroll_to_top(display_row)
|
||||||
|
|
||||||
def solicit_playlist_name(
|
def solicit_playlist_name(
|
||||||
self, session: scoped_session, default: str = ""
|
self, session: scoped_session, default: str = ""
|
||||||
@ -1382,6 +1412,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.playing = False
|
self.playing = False
|
||||||
else:
|
else:
|
||||||
# Return if not playing
|
# Return if not playing
|
||||||
|
log.error("stop_playing() called but not playing")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Stop/fade track
|
# Stop/fade track
|
||||||
@ -1401,7 +1432,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
track_sequence.now = PlaylistTrack()
|
track_sequence.now = PlaylistTrack()
|
||||||
|
|
||||||
# Tell model previous track has finished
|
# Tell model previous track has finished
|
||||||
self.active_model().previous_track_ended()
|
self.active_proxy_model().previous_track_ended()
|
||||||
|
|
||||||
# Reset clocks
|
# Reset clocks
|
||||||
self.frame_fade.setStyleSheet("")
|
self.frame_fade.setStyleSheet("")
|
||||||
@ -1416,6 +1447,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
# Enable controls
|
# Enable controls
|
||||||
self.enable_play_next_controls()
|
self.enable_play_next_controls()
|
||||||
|
|
||||||
|
def tab_change(self):
|
||||||
|
"""Called when active tab changed"""
|
||||||
|
|
||||||
|
log.info("tab_change()")
|
||||||
|
|
||||||
|
tab = self.active_tab()
|
||||||
|
if tab:
|
||||||
|
tab.resizeRowsToContents()
|
||||||
|
|
||||||
def tick_10ms(self) -> None:
|
def tick_10ms(self) -> None:
|
||||||
"""
|
"""
|
||||||
Called every 10ms
|
Called every 10ms
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
# Allow forward reference to PlaylistModel
|
||||||
|
from __future__ import annotations
|
||||||
import obsws_python as obs # type: ignore
|
import obsws_python as obs # type: ignore
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -119,6 +121,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
log.info(f"PlaylistModel.__init__({playlist_id=})")
|
||||||
|
|
||||||
self.playlist_id = playlist_id
|
self.playlist_id = playlist_id
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -150,6 +154,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Add track to existing header row
|
Add track to existing header row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"add_track_to_header({row_number=}, {track_id=}, {note=}")
|
||||||
|
|
||||||
# Get existing row
|
# Get existing row
|
||||||
try:
|
try:
|
||||||
prd = self.playlist_rows[row_number]
|
prd = self.playlist_rows[row_number]
|
||||||
@ -173,10 +179,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Add any further note (header will already have a note)
|
# Add any further note (header will already have a note)
|
||||||
if note:
|
if note:
|
||||||
plr.note += "\n" + note
|
plr.note += "\n" + note
|
||||||
# Reset header row spanning
|
|
||||||
self.signals.span_cells_signal.emit(
|
|
||||||
self.playlist_id, row_number, HEADER_NOTES_COLUMN, 1, 1
|
|
||||||
)
|
|
||||||
# Update local copy
|
# Update local copy
|
||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
# Repaint row
|
# Repaint row
|
||||||
@ -283,6 +285,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if plr:
|
if plr:
|
||||||
plr.played = True
|
plr.played = True
|
||||||
self.refresh_row(session, plr.plr_rownum)
|
self.refresh_row(session, plr.plr_rownum)
|
||||||
|
else:
|
||||||
|
log.error(f"Can't retrieve plr, {track_sequence.now.plr_id=}")
|
||||||
|
|
||||||
# Update track times
|
# Update track times
|
||||||
self.start_end_times[row_number].start_time = track_sequence.now.start_time
|
self.start_end_times[row_number].start_time = track_sequence.now.start_time
|
||||||
@ -365,6 +369,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
calls. To keep it simple, if inefficient, delete rows one by one.
|
calls. To keep it simple, if inefficient, delete rows one by one.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"delete_rows({row_numbers=}")
|
||||||
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
for row_number in row_numbers:
|
for row_number in row_numbers:
|
||||||
super().beginRemoveRows(QModelIndex(), row_number, row_number)
|
super().beginRemoveRows(QModelIndex(), row_number, row_number)
|
||||||
@ -380,15 +386,19 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Return text for display
|
Return text for display
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.debug(f"display_role({row=}, {column=}")
|
||||||
|
|
||||||
|
# Set / reset column span
|
||||||
|
if column == HEADER_NOTES_COLUMN:
|
||||||
|
column_span = 1
|
||||||
|
if self.is_header_row(row):
|
||||||
|
column_span = self.columnCount() - 1
|
||||||
|
self.signals.span_cells_signal.emit(
|
||||||
|
self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span
|
||||||
|
)
|
||||||
|
|
||||||
if self.is_header_row(row):
|
if self.is_header_row(row):
|
||||||
if column == HEADER_NOTES_COLUMN:
|
if column == HEADER_NOTES_COLUMN:
|
||||||
self.signals.span_cells_signal.emit(
|
|
||||||
self.playlist_id,
|
|
||||||
row,
|
|
||||||
HEADER_NOTES_COLUMN,
|
|
||||||
1,
|
|
||||||
self.columnCount() - 1,
|
|
||||||
)
|
|
||||||
header_text = self.header_text(prd)
|
header_text = self.header_text(prd)
|
||||||
if not header_text:
|
if not header_text:
|
||||||
return QVariant(Config.TEXT_NO_TRACK_NO_NOTE)
|
return QVariant(Config.TEXT_NO_TRACK_NO_NOTE)
|
||||||
@ -494,6 +504,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
(ie, ignore the first, not-yet-duplicate, track).
|
(ie, ignore the first, not-yet-duplicate, track).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info("get_duplicate_rows() called")
|
||||||
|
|
||||||
found = []
|
found = []
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
@ -506,6 +518,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
else:
|
else:
|
||||||
found.append(track_id)
|
found.append(track_id)
|
||||||
|
|
||||||
|
log.info(f"get_duplicate_rows() returned: {result=}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
|
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
|
||||||
@ -516,6 +529,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
If not given, return row number to add to end of model.
|
If not given, return row number to add to end of model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"_get_new_row_number({proposed_row_number=})")
|
||||||
|
|
||||||
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
|
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
|
||||||
# We are adding to the end of the list
|
# We are adding to the end of the list
|
||||||
new_row_number = len(self.playlist_rows)
|
new_row_number = len(self.playlist_rows)
|
||||||
@ -525,6 +540,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
else:
|
else:
|
||||||
new_row_number = proposed_row_number
|
new_row_number = proposed_row_number
|
||||||
|
|
||||||
|
log.info(f"get_new_row_number() return: {new_row_number=}")
|
||||||
return new_row_number
|
return new_row_number
|
||||||
|
|
||||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||||
@ -557,7 +573,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Return a list of unplayed row numbers
|
Return a list of unplayed row numbers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [a.plr_rownum for a in self.playlist_rows.values() if not a.played]
|
result = [a.plr_rownum for a in self.playlist_rows.values() if not a.played]
|
||||||
|
log.info(f"get_unplayed_rows() returned: {result=}")
|
||||||
|
return result
|
||||||
|
|
||||||
def headerData(
|
def headerData(
|
||||||
self,
|
self,
|
||||||
@ -716,6 +734,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Insert a row.
|
Insert a row.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"insert_row({proposed_row_number=}, {track_id=}, {note=})")
|
||||||
|
|
||||||
new_row_number = self._get_new_row_number(proposed_row_number)
|
new_row_number = self._get_new_row_number(proposed_row_number)
|
||||||
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
@ -779,11 +799,28 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Mark row as unplayed
|
||||||
|
"""
|
||||||
|
|
||||||
|
with Session() as session:
|
||||||
|
for row_number in row_numbers:
|
||||||
|
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||||
|
if not plr:
|
||||||
|
return
|
||||||
|
plr.played = False
|
||||||
|
self.refresh_row(session, row_number)
|
||||||
|
|
||||||
|
self.invalidate_rows(row_numbers)
|
||||||
|
|
||||||
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Move the playlist rows given to to_row and below.
|
Move the playlist rows given to to_row and below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"move_rows({from_rows=}, {to_row_number=}")
|
||||||
|
|
||||||
# Build a {current_row_number: new_row_number} dictionary
|
# Build a {current_row_number: new_row_number} dictionary
|
||||||
row_map: dict[int, int] = {}
|
row_map: dict[int, int] = {}
|
||||||
|
|
||||||
@ -813,13 +850,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if old_row != new_row:
|
if old_row != new_row:
|
||||||
row_map[old_row] = new_row
|
row_map[old_row] = new_row
|
||||||
|
|
||||||
# Reset any header rows that we're moving
|
|
||||||
for moving_row in row_map:
|
|
||||||
if self.is_header_row(moving_row):
|
|
||||||
# Reset column span
|
|
||||||
self.signals.span_cells_signal.emit(
|
|
||||||
self.playlist_id, moving_row, HEADER_NOTES_COLUMN, 1, 1
|
|
||||||
)
|
|
||||||
# Check to see whether any rows in track_sequence have moved
|
# Check to see whether any rows in track_sequence have moved
|
||||||
if track_sequence.previous.plr_rownum in row_map:
|
if track_sequence.previous.plr_rownum in row_map:
|
||||||
track_sequence.previous.plr_rownum = row_map[
|
track_sequence.previous.plr_rownum = row_map[
|
||||||
@ -846,28 +876,19 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.reset_track_sequence_row_numbers()
|
self.reset_track_sequence_row_numbers()
|
||||||
self.invalidate_rows(list(row_map.keys()))
|
self.invalidate_rows(list(row_map.keys()))
|
||||||
|
|
||||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
|
||||||
"""
|
|
||||||
Mark row as unplayed
|
|
||||||
"""
|
|
||||||
|
|
||||||
with Session() as session:
|
|
||||||
for row_number in row_numbers:
|
|
||||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
|
||||||
if not plr:
|
|
||||||
return
|
|
||||||
plr.played = False
|
|
||||||
self.refresh_row(session, row_number)
|
|
||||||
|
|
||||||
self.invalidate_rows(row_numbers)
|
|
||||||
|
|
||||||
def move_rows_between_playlists(
|
def move_rows_between_playlists(
|
||||||
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
self, from_rows: List[int], to_row_number: int, to_playlist_model: PlaylistModel
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Move the playlist rows given to to_row and below of to_playlist.
|
Move the playlist rows given to to_row and below of to_playlist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f"move_rows_between_playlists({from_rows=}, {to_row_number=}, {to_playlist_model=}"
|
||||||
|
)
|
||||||
|
|
||||||
|
to_playlist_id = to_playlist_model.playlist_id
|
||||||
|
|
||||||
# Row removal must be wrapped in beginRemoveRows ..
|
# Row removal must be wrapped in beginRemoveRows ..
|
||||||
# endRemoveRows and the row range must be contiguous. Process
|
# endRemoveRows and the row range must be contiguous. Process
|
||||||
# the highest rows first so the lower row numbers are unchanged
|
# the highest rows first so the lower row numbers are unchanged
|
||||||
@ -886,6 +907,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
max_destination_row_number
|
max_destination_row_number
|
||||||
and to_row_number <= max_destination_row_number
|
and to_row_number <= max_destination_row_number
|
||||||
):
|
):
|
||||||
|
# Move the destination playlist rows down to make room.
|
||||||
PlaylistRows.move_rows_down(
|
PlaylistRows.move_rows_down(
|
||||||
session, to_playlist_id, to_row_number, len(from_rows)
|
session, to_playlist_id, to_row_number, len(from_rows)
|
||||||
)
|
)
|
||||||
@ -923,6 +945,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Move existing_prd track to new_row_number and append note to any existing note
|
Move existing_prd track to new_row_number and append note to any existing note
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"move_track_add_note({new_row_number=}, {existing_prd=}, {note=}")
|
||||||
|
|
||||||
if note:
|
if note:
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
plr = session.get(PlaylistRows, existing_prd.plrid)
|
plr = session.get(PlaylistRows, existing_prd.plrid)
|
||||||
@ -944,6 +968,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Add the existing_prd track details to the existing header at header_row_number
|
Add the existing_prd track details to the existing header at header_row_number
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"move_track_to_header({header_row_number=}, {existing_prd=}, {note=}")
|
||||||
|
|
||||||
if existing_prd.track_id:
|
if existing_prd.track_id:
|
||||||
if note and existing_prd.note:
|
if note and existing_prd.note:
|
||||||
note += "\n" + existing_prd.note
|
note += "\n" + existing_prd.note
|
||||||
@ -956,6 +982,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
and execute any found
|
and execute any found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"obs_scene_change({row_number=})")
|
||||||
|
|
||||||
# Check any headers before this row
|
# Check any headers before this row
|
||||||
idx = row_number - 1
|
idx = row_number - 1
|
||||||
while self.is_header_row(idx):
|
while self.is_header_row(idx):
|
||||||
@ -992,6 +1020,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
- update display
|
- update display
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info("previous_track_ended()")
|
||||||
|
|
||||||
# Sanity check
|
# Sanity check
|
||||||
if not track_sequence.previous.track_id:
|
if not track_sequence.previous.track_id:
|
||||||
log.error("playlistmodel:previous_track_ended called with no current track")
|
log.error("playlistmodel:previous_track_ended called with no current track")
|
||||||
@ -1025,6 +1055,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Remove track from row, retaining row as a header row
|
Remove track from row, retaining row as a header row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"remove_track({row_number=})")
|
||||||
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||||
if plr:
|
if plr:
|
||||||
@ -1051,6 +1083,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Signal handler for when row ordering has changed
|
Signal handler for when row ordering has changed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info("reset_track_sequence_row_numbers()")
|
||||||
|
|
||||||
# Check the track_sequence next, now and previous plrs and
|
# Check the track_sequence next, now and previous plrs and
|
||||||
# update the row number
|
# update the row number
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
@ -1081,6 +1115,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"_reversed_contiguous_row_groups({row_numbers=} called")
|
||||||
|
|
||||||
result: List[List[int]] = []
|
result: List[List[int]] = []
|
||||||
temp: List[int] = []
|
temp: List[int] = []
|
||||||
last_value = row_numbers[0] - 1
|
last_value = row_numbers[0] - 1
|
||||||
@ -1095,6 +1131,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
result.append(temp)
|
result.append(temp)
|
||||||
result.reverse()
|
result.reverse()
|
||||||
|
|
||||||
|
log.info(f"_reversed_contiguous_row_groups() returned: {result=}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
||||||
@ -1107,6 +1144,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Signal handler for when row ordering has changed
|
Signal handler for when row ordering has changed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"row_order_changed({playlist_id=}) {self.playlist_id=}")
|
||||||
|
|
||||||
# Only action if this is for us
|
# Only action if this is for us
|
||||||
if playlist_id != self.playlist_id:
|
if playlist_id != self.playlist_id:
|
||||||
return
|
return
|
||||||
@ -1141,6 +1180,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Set row_number as next track. If row_number is None, clear next track.
|
Set row_number as next track. If row_number is None, clear next track.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"set_next_row({row_number=})")
|
||||||
|
|
||||||
next_row_was = track_sequence.next.plr_rownum
|
next_row_was = track_sequence.next.plr_rownum
|
||||||
|
|
||||||
if row_number is None:
|
if row_number is None:
|
||||||
@ -1279,6 +1320,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Update track start/end times in self.playlist_rows
|
Update track start/end times in self.playlist_rows
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info("update_track_times()")
|
||||||
|
|
||||||
next_start_time: Optional[datetime] = None
|
next_start_time: Optional[datetime] = None
|
||||||
update_rows: List[int] = []
|
update_rows: List[int] = []
|
||||||
|
|
||||||
@ -1360,24 +1403,27 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data_model: PlaylistModel,
|
source_model: PlaylistModel,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.data_model = data_model
|
self.source_model = source_model
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.setSourceModel(data_model)
|
self.setSourceModel(source_model)
|
||||||
# Search all columns
|
# Search all columns
|
||||||
self.setFilterKeyColumn(-1)
|
self.setFilterKeyColumn(-1)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (f"<PlaylistProxyModel: source_model={self.source_model}>")
|
||||||
|
|
||||||
def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool:
|
def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool:
|
||||||
"""
|
"""
|
||||||
Subclass to filter by played status
|
Subclass to filter by played status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.data_model.played_tracks_hidden:
|
if self.source_model.played_tracks_hidden:
|
||||||
if self.data_model.is_played_row(source_row):
|
if self.source_model.is_played_row(source_row):
|
||||||
# Don't hide current or next track
|
# Don't hide current or next track
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
if track_sequence.next.plr_id:
|
if track_sequence.next.plr_id:
|
||||||
@ -1385,7 +1431,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
if (
|
if (
|
||||||
next_plr
|
next_plr
|
||||||
and next_plr.plr_rownum == source_row
|
and next_plr.plr_rownum == source_row
|
||||||
and next_plr.playlist_id == self.data_model.playlist_id
|
and next_plr.playlist_id == self.source_model.playlist_id
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
if track_sequence.now.plr_id:
|
if track_sequence.now.plr_id:
|
||||||
@ -1393,7 +1439,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
if (
|
if (
|
||||||
now_plr
|
now_plr
|
||||||
and now_plr.plr_rownum == source_row
|
and now_plr.plr_rownum == source_row
|
||||||
and now_plr.playlist_id == self.data_model.playlist_id
|
and now_plr.playlist_id == self.source_model.playlist_id
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
# Don't hide previous track until
|
# Don't hide previous track until
|
||||||
@ -1406,7 +1452,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
if (
|
if (
|
||||||
previous_plr
|
previous_plr
|
||||||
and previous_plr.plr_rownum == source_row
|
and previous_plr.plr_rownum == source_row
|
||||||
and previous_plr.playlist_id == self.data_model.playlist_id
|
and previous_plr.playlist_id
|
||||||
|
== self.source_model.playlist_id
|
||||||
):
|
):
|
||||||
if track_sequence.now.start_time:
|
if track_sequence.now.start_time:
|
||||||
if datetime.now() > (
|
if datetime.now() > (
|
||||||
@ -1425,7 +1472,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
# true next time through.
|
# true next time through.
|
||||||
QTimer.singleShot(
|
QTimer.singleShot(
|
||||||
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
||||||
lambda: self.data_model.invalidate_row(
|
lambda: self.source_model.invalidate_row(
|
||||||
source_row
|
source_row
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -1452,28 +1499,28 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
# ######################################
|
# ######################################
|
||||||
|
|
||||||
def current_track_started(self):
|
def current_track_started(self):
|
||||||
return self.data_model.current_track_started()
|
return self.source_model.current_track_started()
|
||||||
|
|
||||||
def delete_rows(self, row_numbers: List[int]) -> None:
|
def delete_rows(self, row_numbers: List[int]) -> None:
|
||||||
return self.data_model.delete_rows(row_numbers)
|
return self.source_model.delete_rows(row_numbers)
|
||||||
|
|
||||||
def get_duplicate_rows(self) -> List[int]:
|
def get_duplicate_rows(self) -> List[int]:
|
||||||
return self.data_model.get_duplicate_rows()
|
return self.source_model.get_duplicate_rows()
|
||||||
|
|
||||||
def get_rows_duration(self, row_numbers: List[int]) -> int:
|
def get_rows_duration(self, row_numbers: List[int]) -> int:
|
||||||
return self.data_model.get_rows_duration(row_numbers)
|
return self.source_model.get_rows_duration(row_numbers)
|
||||||
|
|
||||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||||
return self.data_model.get_row_info(row_number)
|
return self.source_model.get_row_info(row_number)
|
||||||
|
|
||||||
def get_row_track_path(self, row_number: int) -> str:
|
def get_row_track_path(self, row_number: int) -> str:
|
||||||
return self.data_model.get_row_track_path(row_number)
|
return self.source_model.get_row_track_path(row_number)
|
||||||
|
|
||||||
def get_unplayed_rows(self) -> List[int]:
|
def get_unplayed_rows(self) -> List[int]:
|
||||||
return self.data_model.get_unplayed_rows()
|
return self.source_model.get_unplayed_rows()
|
||||||
|
|
||||||
def hide_played_tracks(self, hide: bool) -> None:
|
def hide_played_tracks(self, hide: bool) -> None:
|
||||||
return self.data_model.hide_played_tracks(hide)
|
return self.source_model.hide_played_tracks(hide)
|
||||||
|
|
||||||
def insert_row(
|
def insert_row(
|
||||||
self,
|
self,
|
||||||
@ -1481,65 +1528,65 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
track_id: Optional[int] = None,
|
track_id: Optional[int] = None,
|
||||||
note: Optional[str] = None,
|
note: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.data_model.insert_row(proposed_row_number, track_id, note)
|
return self.source_model.insert_row(proposed_row_number, track_id, note)
|
||||||
|
|
||||||
def is_header_row(self, row_number: int) -> bool:
|
def is_header_row(self, row_number: int) -> bool:
|
||||||
return self.data_model.is_header_row(row_number)
|
return self.source_model.is_header_row(row_number)
|
||||||
|
|
||||||
def is_played_row(self, row_number: int) -> bool:
|
def is_played_row(self, row_number: int) -> bool:
|
||||||
return self.data_model.is_played_row(row_number)
|
return self.source_model.is_played_row(row_number)
|
||||||
|
|
||||||
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
||||||
return self.data_model.is_track_in_playlist(track_id)
|
return self.source_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.data_model.mark_unplayed(row_numbers)
|
return self.source_model.mark_unplayed(row_numbers)
|
||||||
|
|
||||||
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
||||||
return self.data_model.move_rows(from_rows, to_row_number)
|
return self.source_model.move_rows(from_rows, to_row_number)
|
||||||
|
|
||||||
def move_rows_between_playlists(
|
def move_rows_between_playlists(
|
||||||
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.data_model.move_rows_between_playlists(
|
return self.source_model.move_rows_between_playlists(
|
||||||
from_rows, to_row_number, to_playlist_id
|
from_rows, to_row_number, to_playlist_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def move_track_add_note(
|
def move_track_add_note(
|
||||||
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
|
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.data_model.move_track_add_note(new_row_number, existing_prd, note)
|
return self.source_model.move_track_add_note(new_row_number, existing_prd, note)
|
||||||
|
|
||||||
def move_track_to_header(
|
def move_track_to_header(
|
||||||
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
|
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.data_model.move_track_to_header(
|
return self.source_model.move_track_to_header(
|
||||||
header_row_number, existing_prd, note
|
header_row_number, existing_prd, note
|
||||||
)
|
)
|
||||||
|
|
||||||
def previous_track_ended(self) -> None:
|
def previous_track_ended(self) -> None:
|
||||||
return self.data_model.previous_track_ended()
|
return self.source_model.previous_track_ended()
|
||||||
|
|
||||||
def remove_track(self, row_number: int) -> None:
|
def remove_track(self, row_number: int) -> None:
|
||||||
return self.data_model.remove_track(row_number)
|
return self.source_model.remove_track(row_number)
|
||||||
|
|
||||||
def rescan_track(self, row_number: int) -> None:
|
def rescan_track(self, row_number: int) -> None:
|
||||||
return self.data_model.rescan_track(row_number)
|
return self.source_model.rescan_track(row_number)
|
||||||
|
|
||||||
def set_next_row(self, row_number: Optional[int]) -> None:
|
def set_next_row(self, row_number: Optional[int]) -> None:
|
||||||
return self.data_model.set_next_row(row_number)
|
return self.source_model.set_next_row(row_number)
|
||||||
|
|
||||||
def sort_by_artist(self, row_numbers: List[int]) -> None:
|
def sort_by_artist(self, row_numbers: List[int]) -> None:
|
||||||
return self.data_model.sort_by_artist(row_numbers)
|
return self.source_model.sort_by_artist(row_numbers)
|
||||||
|
|
||||||
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
||||||
return self.data_model.sort_by_duration(row_numbers)
|
return self.source_model.sort_by_duration(row_numbers)
|
||||||
|
|
||||||
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
||||||
return self.data_model.sort_by_lastplayed(row_numbers)
|
return self.source_model.sort_by_lastplayed(row_numbers)
|
||||||
|
|
||||||
def sort_by_title(self, row_numbers: List[int]) -> None:
|
def sort_by_title(self, row_numbers: List[int]) -> None:
|
||||||
return self.data_model.sort_by_title(row_numbers)
|
return self.source_model.sort_by_title(row_numbers)
|
||||||
|
|
||||||
def update_track_times(self) -> None:
|
def update_track_times(self) -> None:
|
||||||
return self.data_model.update_track_times()
|
return self.source_model.update_track_times()
|
||||||
|
|||||||
227
app/playlists.py
227
app/playlists.py
@ -40,6 +40,7 @@ from helpers import (
|
|||||||
show_OK,
|
show_OK,
|
||||||
show_warning,
|
show_warning,
|
||||||
)
|
)
|
||||||
|
from log import log
|
||||||
from models import Settings
|
from models import Settings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -54,9 +55,9 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
- checks with user before abandoning edit on Escape
|
- checks with user before abandoning edit on Escape
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent, data_model: PlaylistModel) -> None:
|
def __init__(self, parent, source_model: PlaylistModel) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.data_model = data_model
|
self.source_model = source_model
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
|
|
||||||
def createEditor(
|
def createEditor(
|
||||||
@ -77,7 +78,8 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
row = index.row()
|
row = index.row()
|
||||||
row_height = p.rowHeight(row)
|
row_height = p.rowHeight(row)
|
||||||
p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT)
|
p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT)
|
||||||
return QPlainTextEdit(parent)
|
self.editor = QPlainTextEdit(parent)
|
||||||
|
return self.editor
|
||||||
return super().createEditor(parent, option, index)
|
return super().createEditor(parent, option, index)
|
||||||
|
|
||||||
def destroyEditor(self, editor: Optional[QWidget], index: QModelIndex) -> None:
|
def destroyEditor(self, editor: Optional[QWidget], index: QModelIndex) -> None:
|
||||||
@ -102,6 +104,10 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
self.closeEditor.emit(editor)
|
self.closeEditor.emit(editor)
|
||||||
return True
|
return True
|
||||||
elif key_event.key() == Qt.Key.Key_Escape:
|
elif key_event.key() == Qt.Key.Key_Escape:
|
||||||
|
if self.original_text == self.editor.toPlainText():
|
||||||
|
# No changes made
|
||||||
|
self.closeEditor.emit(editor)
|
||||||
|
return True
|
||||||
discard_edits = QMessageBox.question(
|
discard_edits = QMessageBox.question(
|
||||||
cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
|
cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
|
||||||
)
|
)
|
||||||
@ -111,24 +117,20 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def setEditorData(self, editor, index):
|
def setEditorData(self, editor, index):
|
||||||
model = index.model()
|
proxy_model = index.model()
|
||||||
if hasattr(model, "mapToSource"):
|
edit_index = proxy_model.mapToSource(index)
|
||||||
edit_index = model.mapToSource(index)
|
|
||||||
else:
|
|
||||||
edit_index = index
|
|
||||||
|
|
||||||
value = self.data_model.data(edit_index, Qt.ItemDataRole.EditRole)
|
self.original_text = self.source_model.data(
|
||||||
editor.setPlainText(value.value())
|
edit_index, Qt.ItemDataRole.EditRole
|
||||||
|
)
|
||||||
|
editor.setPlainText(self.original_text.value())
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
def setModelData(self, editor, model, index):
|
||||||
model = index.model()
|
proxy_model = index.model()
|
||||||
if hasattr(model, "mapToSource"):
|
edit_index = proxy_model.mapToSource(index)
|
||||||
edit_index = model.mapToSource(index)
|
|
||||||
else:
|
|
||||||
edit_index = index
|
|
||||||
|
|
||||||
value = editor.toPlainText().strip()
|
value = editor.toPlainText().strip()
|
||||||
self.data_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
||||||
|
|
||||||
def updateEditorGeometry(self, editor, option, index):
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
editor.setGeometry(option.rect)
|
editor.setGeometry(option.rect)
|
||||||
@ -167,11 +169,12 @@ class PlaylistTab(QTableView):
|
|||||||
# Save passed settings
|
# Save passed settings
|
||||||
self.musicmuster = musicmuster
|
self.musicmuster = musicmuster
|
||||||
self.playlist_id = playlist_id
|
self.playlist_id = playlist_id
|
||||||
|
log.info(f"PlaylistTab.__init__({playlist_id=})")
|
||||||
|
|
||||||
# Set up widget
|
# Set up widget
|
||||||
self.data_model = PlaylistModel(playlist_id)
|
self.source_model = PlaylistModel(playlist_id)
|
||||||
self.proxy_model = PlaylistProxyModel(self.data_model)
|
self.proxy_model = PlaylistProxyModel(self.source_model)
|
||||||
self.setItemDelegate(EscapeDelegate(self, self.data_model))
|
self.setItemDelegate(EscapeDelegate(self, self.source_model))
|
||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||||
@ -188,14 +191,8 @@ class PlaylistTab(QTableView):
|
|||||||
self.customContextMenuRequested.connect(self._context_menu)
|
self.customContextMenuRequested.connect(self._context_menu)
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
# This dancing is to satisfy mypy
|
|
||||||
h_header = self.horizontalHeader()
|
|
||||||
if isinstance(h_header, QHeaderView):
|
|
||||||
h_header.sectionResized.connect(self._column_resize)
|
|
||||||
h_header.setStretchLastSection(True)
|
|
||||||
# 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.resize_rows_signal.connect(self.resize_rows)
|
||||||
self.signals.span_cells_signal.connect(self._span_cells)
|
self.signals.span_cells_signal.connect(self._span_cells)
|
||||||
|
|
||||||
# Selection model
|
# Selection model
|
||||||
@ -205,9 +202,20 @@ class PlaylistTab(QTableView):
|
|||||||
# Load playlist rows
|
# Load playlist rows
|
||||||
self.setModel(self.proxy_model)
|
self.setModel(self.proxy_model)
|
||||||
self._set_column_widths()
|
self._set_column_widths()
|
||||||
QTimer.singleShot(0, lambda: self.resizeRowsToContents())
|
# Stretch last column *after* setting column widths which is
|
||||||
|
# *much* faster
|
||||||
|
h_header = self.horizontalHeader()
|
||||||
|
if isinstance(h_header, QHeaderView):
|
||||||
|
h_header.sectionResized.connect(self._column_resize)
|
||||||
|
h_header.setStretchLastSection(True)
|
||||||
|
# Setting ResizeToContents causes screen flash on load
|
||||||
|
# v_header = self.verticalHeader()
|
||||||
|
# if v_header:
|
||||||
|
# print("HEADER")
|
||||||
|
# v_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
QTimer.singleShot(300, self.resizeRowsToContents)
|
||||||
|
|
||||||
# ########## Overrident class functions ##########
|
# ########## Overridden class functions ##########
|
||||||
|
|
||||||
def closeEditor(
|
def closeEditor(
|
||||||
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
|
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
|
||||||
@ -225,7 +233,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Update start times in case a start time in a note has been
|
# Update start times in case a start time in a note has been
|
||||||
# edited
|
# edited
|
||||||
self.data_model.update_track_times()
|
self.source_model.update_track_times()
|
||||||
|
|
||||||
# Deselect edited line
|
# Deselect edited line
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
@ -240,13 +248,14 @@ class PlaylistTab(QTableView):
|
|||||||
from_rows = self.selected_model_row_numbers()
|
from_rows = self.selected_model_row_numbers()
|
||||||
to_index = self.indexAt(event.position().toPoint())
|
to_index = self.indexAt(event.position().toPoint())
|
||||||
to_model_row = self.proxy_model.mapToSource(to_index).row()
|
to_model_row = self.proxy_model.mapToSource(to_index).row()
|
||||||
|
log.info(f"PlaylistTab.dropEvent(): {from_rows=}, {to_index=}, {to_model_row=}")
|
||||||
|
|
||||||
if (
|
if (
|
||||||
0 <= min(from_rows) <= self.data_model.rowCount()
|
0 <= min(from_rows) <= self.source_model.rowCount()
|
||||||
and 0 <= max(from_rows) <= self.data_model.rowCount()
|
and 0 <= max(from_rows) <= self.source_model.rowCount()
|
||||||
and 0 <= to_model_row <= self.data_model.rowCount()
|
and 0 <= to_model_row <= self.source_model.rowCount()
|
||||||
):
|
):
|
||||||
self.data_model.move_rows(from_rows, to_model_row)
|
self.source_model.move_rows(from_rows, to_model_row)
|
||||||
|
|
||||||
# Reset drag mode to allow row selection by dragging
|
# Reset drag mode to allow row selection by dragging
|
||||||
self.setDragEnabled(False)
|
self.setDragEnabled(False)
|
||||||
@ -283,7 +292,7 @@ class PlaylistTab(QTableView):
|
|||||||
if len(selected_rows) == 0:
|
if len(selected_rows) == 0:
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
self.musicmuster.lblSumPlaytime.setText("")
|
||||||
else:
|
else:
|
||||||
selected_duration = self.data_model.get_rows_duration(
|
selected_duration = self.source_model.get_rows_duration(
|
||||||
self.get_selected_rows()
|
self.get_selected_rows()
|
||||||
)
|
)
|
||||||
if selected_duration > 0:
|
if selected_duration > 0:
|
||||||
@ -321,7 +330,7 @@ class PlaylistTab(QTableView):
|
|||||||
def _add_track(self) -> None:
|
def _add_track(self) -> None:
|
||||||
"""Add a track to a section header making it a normal track row"""
|
"""Add a track to a section header making it a normal track row"""
|
||||||
|
|
||||||
model_row_number = self.selected_model_row_number()
|
model_row_number = self.source_model_selected_row_number()
|
||||||
if model_row_number is None:
|
if model_row_number is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -329,7 +338,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.data_model,
|
source_model=self.source_model,
|
||||||
add_to_header=True,
|
add_to_header=True,
|
||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
@ -338,24 +347,28 @@ class PlaylistTab(QTableView):
|
|||||||
"""Used to process context (right-click) menu, which is defined here"""
|
"""Used to process context (right-click) menu, which is defined here"""
|
||||||
|
|
||||||
self.menu.clear()
|
self.menu.clear()
|
||||||
model = self.proxy_model
|
proxy_model = self.proxy_model
|
||||||
|
|
||||||
display_row_number = item.row()
|
index = proxy_model.index(item.row(), item.column())
|
||||||
if hasattr(model, "mapToSource"):
|
model_row_number = proxy_model.mapToSource(index).row()
|
||||||
index = model.index(item.row(), item.column())
|
|
||||||
model_row_number = model.mapToSource(index).row()
|
|
||||||
else:
|
|
||||||
model_row_number = display_row_number
|
|
||||||
|
|
||||||
header_row = model.is_header_row(model_row_number)
|
header_row = proxy_model.is_header_row(model_row_number)
|
||||||
track_row = not header_row
|
track_row = not header_row
|
||||||
current_row = model_row_number == track_sequence.now.plr_rownum
|
current_row = model_row_number == track_sequence.now.plr_rownum
|
||||||
next_row = model_row_number == track_sequence.next.plr_rownum
|
next_row = model_row_number == track_sequence.next.plr_rownum
|
||||||
|
track_path = self.source_model.get_row_info(model_row_number).path
|
||||||
|
|
||||||
# Open in Audacity
|
# Open/import in/from Audacity
|
||||||
if track_row and not current_row:
|
if track_row and not current_row:
|
||||||
|
if track_path == self.musicmuster.audacity_file_path:
|
||||||
|
# This track was opened in Audacity
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Open in Audacity", lambda: self.open_in_audacity(model_row_number)
|
"Update from Audacity",
|
||||||
|
lambda: self._import_from_audacity(model_row_number),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._add_context_menu(
|
||||||
|
"Open in Audacity", lambda: self._open_in_audacity(model_row_number)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Rescan
|
# Rescan
|
||||||
@ -374,7 +387,8 @@ class PlaylistTab(QTableView):
|
|||||||
# Remove track from row
|
# Remove track from row
|
||||||
if track_row and not current_row and not next_row:
|
if track_row and not current_row and not next_row:
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Remove track from row", lambda: model.remove_track(model_row_number)
|
"Remove track from row",
|
||||||
|
lambda: proxy_model.remove_track(model_row_number),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add track to section header (ie, make this a track row)
|
# Add track to section header (ie, make this a track row)
|
||||||
@ -385,7 +399,7 @@ class PlaylistTab(QTableView):
|
|||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
|
|
||||||
# Mark unplayed
|
# Mark unplayed
|
||||||
if track_row and model.is_played_row(model_row_number):
|
if track_row and proxy_model.is_played_row(model_row_number):
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Mark unplayed",
|
"Mark unplayed",
|
||||||
lambda: self._mark_as_unplayed(self.get_selected_rows()),
|
lambda: self._mark_as_unplayed(self.get_selected_rows()),
|
||||||
@ -404,22 +418,22 @@ class PlaylistTab(QTableView):
|
|||||||
sort_menu = self.menu.addMenu("Sort")
|
sort_menu = self.menu.addMenu("Sort")
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by title",
|
"by title",
|
||||||
lambda: model.sort_by_title(self.get_selected_rows()),
|
lambda: proxy_model.sort_by_title(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by artist",
|
"by artist",
|
||||||
lambda: model.sort_by_artist(self.get_selected_rows()),
|
lambda: proxy_model.sort_by_artist(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by duration",
|
"by duration",
|
||||||
lambda: model.sort_by_duration(self.get_selected_rows()),
|
lambda: proxy_model.sort_by_duration(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by last played",
|
"by last played",
|
||||||
lambda: model.sort_by_lastplayed(self.get_selected_rows()),
|
lambda: proxy_model.sort_by_lastplayed(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -449,6 +463,8 @@ class PlaylistTab(QTableView):
|
|||||||
Called when column width changes. Save new width to database.
|
Called when column width changes. Save new width to database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.info(f"_column_resize({column_number=}, {_old=}, {_new=}")
|
||||||
|
|
||||||
header = self.horizontalHeader()
|
header = self.horizontalHeader()
|
||||||
if not header:
|
if not header:
|
||||||
return
|
return
|
||||||
@ -474,7 +490,7 @@ class PlaylistTab(QTableView):
|
|||||||
to the clipboard. Otherwise, return None.
|
to the clipboard. Otherwise, return None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
track_path = self.data_model.get_row_info(row_number).path
|
track_path = self.source_model.get_row_info(row_number).path
|
||||||
if not track_path:
|
if not track_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -502,6 +518,7 @@ class PlaylistTab(QTableView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
rows_to_delete = self.get_selected_rows()
|
rows_to_delete = self.get_selected_rows()
|
||||||
|
log.info(f"_delete_rows({rows_to_delete=}")
|
||||||
row_count = len(rows_to_delete)
|
row_count = len(rows_to_delete)
|
||||||
if row_count < 1:
|
if row_count < 1:
|
||||||
return
|
return
|
||||||
@ -511,7 +528,7 @@ class PlaylistTab(QTableView):
|
|||||||
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.data_model.delete_rows(self.selected_model_row_numbers())
|
self.source_model.delete_rows(self.selected_model_row_numbers())
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def get_selected_row_track_path(self) -> str:
|
def get_selected_row_track_path(self) -> str:
|
||||||
@ -520,17 +537,25 @@ class PlaylistTab(QTableView):
|
|||||||
row does not have a track, return empty string.
|
row does not have a track, return empty string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_row_number = self.selected_model_row_number()
|
log.info("get_selected_row_track_path() called")
|
||||||
|
|
||||||
|
model_row_number = self.source_model_selected_row_number()
|
||||||
if model_row_number is None:
|
if model_row_number is None:
|
||||||
return ""
|
result = ""
|
||||||
return self.data_model.get_row_track_path(model_row_number)
|
else:
|
||||||
|
result = self.source_model.get_row_track_path(model_row_number)
|
||||||
|
|
||||||
|
log.info(f"get_selected_row_track_path() returned: {result=}")
|
||||||
|
return result
|
||||||
|
|
||||||
def get_selected_rows(self) -> List[int]:
|
def get_selected_rows(self) -> List[int]:
|
||||||
"""Return a list of model-selected row numbers sorted by row"""
|
"""Return a list of model-selected row numbers sorted by row"""
|
||||||
|
|
||||||
|
log.info("get_selected_rows() called")
|
||||||
|
|
||||||
# Use a set to deduplicate result (a selected row will have all
|
# Use a set to deduplicate result (a selected row will have all
|
||||||
# items in that row selected)
|
# items in that row selected)
|
||||||
return sorted(
|
result = sorted(
|
||||||
list(
|
list(
|
||||||
set(
|
set(
|
||||||
[
|
[
|
||||||
@ -541,10 +566,31 @@ class PlaylistTab(QTableView):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.info(f"get_selected_rows() returned: {result=}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _import_from_audacity(self, row_number: int) -> None:
|
||||||
|
"""
|
||||||
|
Import current Audacity track to passed row
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Notify user if audacity not running
|
||||||
|
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
||||||
|
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
|
||||||
|
return
|
||||||
|
|
||||||
|
audacity = self.audacity
|
||||||
|
if not audacity:
|
||||||
|
return
|
||||||
|
|
||||||
|
audacity.export_file()
|
||||||
|
self.musicmuster.audacity_file_path = None
|
||||||
|
self._rescan(row_number)
|
||||||
|
|
||||||
def _info_row(self, row_number: int) -> None:
|
def _info_row(self, row_number: int) -> None:
|
||||||
"""Display popup with info re row"""
|
"""Display popup with info re row"""
|
||||||
|
|
||||||
prd = self.data_model.get_row_info(row_number)
|
prd = self.source_model.get_row_info(row_number)
|
||||||
if prd:
|
if prd:
|
||||||
txt = (
|
txt = (
|
||||||
f"Title: {prd.title}\n"
|
f"Title: {prd.title}\n"
|
||||||
@ -561,12 +607,12 @@ class PlaylistTab(QTableView):
|
|||||||
show_OK(self.musicmuster, "Track info", txt)
|
show_OK(self.musicmuster, "Track info", txt)
|
||||||
|
|
||||||
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
"""Rescan track"""
|
"""Mark row as unplayed"""
|
||||||
|
|
||||||
self.data_model.mark_unplayed(row_numbers)
|
self.source_model.mark_unplayed(row_numbers)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def open_in_audacity(self, row_number: int) -> None:
|
def _open_in_audacity(self, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Open track in passed row in Audacity
|
Open track in passed row in Audacity
|
||||||
"""
|
"""
|
||||||
@ -576,26 +622,32 @@ class PlaylistTab(QTableView):
|
|||||||
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
|
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
|
||||||
return
|
return
|
||||||
|
|
||||||
path = self.data_model.get_row_track_path(row_number)
|
path = self.source_model.get_row_track_path(row_number)
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
|
|
||||||
audacity = AudacityManager()
|
self.musicmuster.audacity_file_path = path
|
||||||
audacity.open_file(path)
|
self.audacity = AudacityManager()
|
||||||
if ask_yes_no(
|
self.audacity.open_file(path)
|
||||||
"Export file",
|
|
||||||
"Click yes to export file, no to ignore",
|
|
||||||
parent=self.musicmuster,
|
|
||||||
):
|
|
||||||
audacity.export_file()
|
|
||||||
self._rescan(row_number)
|
|
||||||
|
|
||||||
def _rescan(self, row_number: int) -> None:
|
def _rescan(self, row_number: int) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.data_model.rescan_track(row_number)
|
self.source_model.rescan_track(row_number)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
|
def resize_rows(self, playlist_id: int) -> None:
|
||||||
|
"""
|
||||||
|
If playlist_id is us, resize rows
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.info(f"resize_rows({playlist_id=}) {self.playlist_id=}")
|
||||||
|
|
||||||
|
if playlist_id != self.playlist_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.resizeRowsToContents()
|
||||||
|
|
||||||
def scroll_to_top(self, row_number: int) -> None:
|
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
|
||||||
@ -620,14 +672,14 @@ class PlaylistTab(QTableView):
|
|||||||
# We need to be in MultiSelection mode
|
# We need to be in MultiSelection mode
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
||||||
# Get the duplicate rows
|
# Get the duplicate rows
|
||||||
duplicate_rows = self.data_model.get_duplicate_rows()
|
duplicate_rows = self.source_model.get_duplicate_rows()
|
||||||
# Select the rows
|
# Select the rows
|
||||||
for duplicate_row in duplicate_rows:
|
for duplicate_row in duplicate_rows:
|
||||||
self.selectRow(duplicate_row)
|
self.selectRow(duplicate_row)
|
||||||
# Reset selection mode
|
# Reset selection mode
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
|
||||||
def selected_model_row_number(self) -> Optional[int]:
|
def source_model_selected_row_number(self) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Return the model row number corresponding to the selected row or None
|
Return the model row number corresponding to the selected row or None
|
||||||
"""
|
"""
|
||||||
@ -635,9 +687,7 @@ class PlaylistTab(QTableView):
|
|||||||
selected_index = self._selected_row_index()
|
selected_index = self._selected_row_index()
|
||||||
if selected_index is None:
|
if selected_index is None:
|
||||||
return None
|
return None
|
||||||
if hasattr(self.proxy_model, "mapToSource"):
|
|
||||||
return self.proxy_model.mapToSource(selected_index).row()
|
return self.proxy_model.mapToSource(selected_index).row()
|
||||||
return selected_index.row()
|
|
||||||
|
|
||||||
def selected_model_row_numbers(self) -> List[int]:
|
def selected_model_row_numbers(self) -> List[int]:
|
||||||
"""
|
"""
|
||||||
@ -682,13 +732,15 @@ class PlaylistTab(QTableView):
|
|||||||
def _set_column_widths(self) -> None:
|
def _set_column_widths(self) -> None:
|
||||||
"""Column widths from settings"""
|
"""Column widths from settings"""
|
||||||
|
|
||||||
|
log.info("_set_column_widths()")
|
||||||
|
|
||||||
header = self.horizontalHeader()
|
header = self.horizontalHeader()
|
||||||
if not header:
|
if not header:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Last column is set to stretch so ignore it here
|
# Last column is set to stretch so ignore it here
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
for column_number in range(header.count() - 2):
|
for column_number in range(header.count() - 1):
|
||||||
attr_name = f"playlist_col_{column_number}_width"
|
attr_name = f"playlist_col_{column_number}_width"
|
||||||
record = Settings.get_int_settings(session, attr_name)
|
record = Settings.get_int_settings(session, attr_name)
|
||||||
if record.f_int is not None:
|
if record.f_int is not None:
|
||||||
@ -701,10 +753,11 @@ class PlaylistTab(QTableView):
|
|||||||
Set selected row as next track
|
Set selected row as next track
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_row_number = self.selected_model_row_number()
|
model_row_number = self.source_model_selected_row_number()
|
||||||
|
log.info(f"set_row_as_next_track() {model_row_number=}")
|
||||||
if model_row_number is None:
|
if model_row_number is None:
|
||||||
return
|
return
|
||||||
self.data_model.set_next_row(model_row_number)
|
self.source_model.set_next_row(model_row_number)
|
||||||
self.clearSelection()
|
self.clearSelection()
|
||||||
|
|
||||||
def _span_cells(
|
def _span_cells(
|
||||||
@ -714,12 +767,18 @@ class PlaylistTab(QTableView):
|
|||||||
Implement spanning of cells, initiated by signal
|
Implement spanning of cells, initiated by signal
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
f"_span_cells({playlist_id=}, {row=}, "
|
||||||
|
f"{column=}, {rowSpan=}, {columnSpan=}) {self.playlist_id=}"
|
||||||
|
)
|
||||||
|
|
||||||
if playlist_id != self.playlist_id:
|
if playlist_id != self.playlist_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
model = self.proxy_model
|
proxy_model = self.proxy_model
|
||||||
if hasattr(model, "mapToSource"):
|
edit_index = proxy_model.mapFromSource(
|
||||||
edit_index = model.mapFromSource(self.data_model.createIndex(row, column))
|
self.source_model.createIndex(row, column)
|
||||||
|
)
|
||||||
row = edit_index.row()
|
row = edit_index.row()
|
||||||
column = edit_index.column()
|
column = edit_index.column()
|
||||||
|
|
||||||
@ -737,5 +796,5 @@ class PlaylistTab(QTableView):
|
|||||||
def _unmark_as_next(self) -> None:
|
def _unmark_as_next(self) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.data_model.set_next_row(None)
|
self.source_model.set_next_row(None)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|||||||
22
poetry.lock
generated
22
poetry.lock
generated
@ -274,7 +274,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
|||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
files = [
|
files = [
|
||||||
@ -282,6 +282,24 @@ files = [
|
|||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorlog"
|
||||||
|
version = "6.8.0"
|
||||||
|
description = "Add colours to the output of Python's logging module."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "colorlog-6.8.0-py3-none-any.whl", hash = "sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375"},
|
||||||
|
{file = "colorlog-6.8.0.tar.gz", hash = "sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decorator"
|
name = "decorator"
|
||||||
version = "5.1.1"
|
version = "5.1.1"
|
||||||
@ -2189,4 +2207,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "5bd0a9ae09f61079a0325639485adb206357cd5ea942944ccb5855f2a83d4db6"
|
content-hash = "3ba4a6affcb5c77a3c0e4b0f7c12d2b7f5d192a68bf7e71eb6a4e58024b5f4e7"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ pyqt6 = "^6.5.0"
|
|||||||
pyqt6-webengine = "^6.5.0"
|
pyqt6-webengine = "^6.5.0"
|
||||||
pygame = "^2.4.0"
|
pygame = "^2.4.0"
|
||||||
pyqtgraph = "^0.13.3"
|
pyqtgraph = "^0.13.3"
|
||||||
|
colorlog = "^6.8.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
ipdb = "^0.13.9"
|
ipdb = "^0.13.9"
|
||||||
|
|||||||
@ -303,7 +303,7 @@ def test_move_one_row_between_playlists_to_end(monkeypatch, session):
|
|||||||
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
||||||
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
||||||
|
|
||||||
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
|
||||||
model_dst.refresh_data(session)
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
||||||
@ -323,7 +323,7 @@ def test_move_one_row_between_playlists_to_middle(monkeypatch, session):
|
|||||||
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
||||||
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
||||||
|
|
||||||
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
|
||||||
model_dst.refresh_data(session)
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
# Check the rows of the destination model
|
# Check the rows of the destination model
|
||||||
@ -347,7 +347,7 @@ def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session):
|
|||||||
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
||||||
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
||||||
|
|
||||||
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
|
||||||
model_dst.refresh_data(session)
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
# Check the rows of the destination model
|
# Check the rows of the destination model
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user