Compare commits

..

No commits in common. "d9ad001c75766bc700c42c9331a2a81b65979172" and "955433686041fa30e1efd43bd6346caa967d9648" have entirely different histories.

7 changed files with 580 additions and 669 deletions

View File

@ -1,176 +0,0 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional
from PyQt6.QtCore import pyqtSignal, QObject
import numpy as np
import pyqtgraph as pg # type: ignore
from config import Config
from dbconfig import scoped_session
from models import PlaylistRows
import helpers
class FadeCurve:
GraphWidget = None
def __init__(self, track):
"""
Set up fade graph array
"""
audio = helpers.get_audio_segment(track.path)
if not audio:
return None
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence
self.start_ms = max(0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
self.end_ms = track.silence_at
self.audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
self.region = None
def clear(self) -> None:
"""Clear the current graph"""
if self.GraphWidget:
self.GraphWidget.clear()
def plot(self):
self.curve = self.GraphWidget.plot(self.graph_array)
self.curve.setPen(Config.FADE_CURVE_FOREGROUND)
def tick(self, play_time) -> None:
"""Update volume fade curve"""
if not self.GraphWidget:
return
ms_of_graph = play_time - self.start_ms
if ms_of_graph < 0:
return
if self.region is None:
# Create the region now that we're into fade
self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
self.GraphWidget.addItem(self.region)
# Update region position
self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor])
@helpers.singleton
@dataclass
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
- https://zetcode.com/gui/pyqt5/eventssignals/
- https://stackoverflow.com/questions/62654525/
emit-a-signal-from-another-class-to-main-class
and Singleton class at
https://refactoring.guru/design-patterns/singleton/python/example#example-0
"""
add_track_to_header_signal = pyqtSignal(int, int, int)
add_track_to_playlist_signal = pyqtSignal(int, int, int, str)
enable_escape_signal = pyqtSignal(bool)
set_next_track_signal = pyqtSignal(int, int)
span_cells_signal = pyqtSignal(int, int, int, int)
def __post_init__(self):
super().__init__()
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
typically the previous, current and next track.
"""
def __init__(self) -> None:
"""
Only initialises data structure. Call set_plr to populate.
Do NOT store row_number here - that changes if tracks are reordered
in playlist (add, remove, drag/drop) and we shouldn't care about row
number: that's the playlist's problem.
"""
self.artist: Optional[str] = None
self.duration: Optional[int] = None
self.end_time: Optional[datetime] = None
self.fade_at: Optional[int] = None
self.fade_curve: Optional[FadeCurve] = None
self.fade_length: Optional[int] = None
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None
self.plr_id: Optional[int] = None
self.silence_at: Optional[int] = None
self.start_gap: Optional[int] = None
self.start_time: Optional[datetime] = None
self.title: Optional[str] = None
self.track_id: Optional[int] = None
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"playlist_id={self.playlist_id}>"
)
def set_plr(self, session: scoped_session, plr: PlaylistRows) -> None:
"""
Update with new plr information
"""
if not plr.track:
return
session.add(plr)
track = plr.track
self.artist = track.artist
self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at
self.fade_graph = FadeCurve(track)
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None
self.title = track.title
self.track_id = track.id
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
def start(self) -> None:
"""
Called when track starts playing
"""
self.start_time = datetime.now()
if self.duration:
self.end_time = self.start_time + timedelta(milliseconds=self.duration)
@helpers.singleton
class CurrentTrack(PlaylistTrack):
pass
@helpers.singleton
class NextTrack(PlaylistTrack):
pass
@helpers.singleton
class PreviousTrack(PlaylistTrack):
pass

20
app/datastructures.py Normal file
View File

@ -0,0 +1,20 @@
from PyQt6.QtCore import pyqtSignal, QObject
from helpers import singleton
@singleton
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
- https://zetcode.com/gui/pyqt5/eventssignals/
- https://stackoverflow.com/questions/62654525/
emit-a-signal-from-another-class-to-main-class
and Singleton class at
https://refactoring.guru/design-patterns/singleton/python/example#example-0
"""
enable_escape_signal = pyqtSignal(bool)
set_next_track_signal = pyqtSignal(int, int)
span_cells_signal = pyqtSignal(int, int, int, int)
add_track_to_playlist_signal = pyqtSignal(int, int, int, str)

View File

@ -1,178 +0,0 @@
from typing import Optional
from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtWidgets import QDialog, QListWidgetItem
from classes import MusicMusterSignals
from dbconfig import scoped_session
from helpers import (
get_relative_date,
ms_to_mmss,
)
from models import Settings, Tracks
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
class TrackSelectDialog(QDialog):
"""Select track from database"""
def __init__(
self,
session: scoped_session,
new_row_number: int,
playlist_id: int,
add_to_header: Optional[bool] = False,
*args,
**kwargs,
) -> None:
"""
Subclassed QDialog to manage track selection
"""
super().__init__(*args, **kwargs)
self.session = session
self.new_row_number = new_row_number
self.playlist_id = playlist_id
self.add_to_header = add_to_header
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.ui.btnAdd.clicked.connect(self.add_selected)
self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)
self.ui.btnClose.clicked.connect(self.close)
self.ui.matchList.itemDoubleClicked.connect(self.add_selected)
self.ui.matchList.itemSelectionChanged.connect(self.selection_changed)
self.ui.radioTitle.toggled.connect(self.title_artist_toggle)
self.ui.searchString.textEdited.connect(self.chars_typed)
self.track: Optional[Tracks] = None
self.signals = MusicMusterSignals()
record = Settings.get_int_settings(self.session, "dbdialog_width")
width = record.f_int or 800
record = Settings.get_int_settings(self.session, "dbdialog_height")
height = record.f_int or 600
self.resize(width, height)
def add_selected(self) -> None:
"""Handle Add button"""
track = None
if self.ui.matchList.selectedItems():
item = self.ui.matchList.currentItem()
if item:
track = item.data(Qt.ItemDataRole.UserRole)
note = self.ui.txtNote.text()
if not note and not track:
return
self.ui.txtNote.clear()
self.select_searchtext()
track_id = None
if track:
track_id = track.id
if self.add_to_header:
self.signals.add_track_to_header_signal.emit(
self.playlist_id, self.new_row_number, track_id
)
else:
self.signals.add_track_to_playlist_signal.emit(
self.playlist_id, self.new_row_number, track_id, note
)
def add_selected_and_close(self) -> None:
"""Handle Add and Close button"""
self.add_selected()
self.accept()
def chars_typed(self, s: str) -> None:
"""Handle text typed in search box"""
self.ui.matchList.clear()
if len(s) > 0:
if self.ui.radioTitle.isChecked():
matches = Tracks.search_titles(self.session, "%" + s)
else:
matches = Tracks.search_artists(self.session, "%" + s)
if matches:
for track in matches:
last_played = None
last_playdate = max(
track.playdates, key=lambda p: p.lastplayed, default=None
)
if last_playdate:
last_played = last_playdate.lastplayed
t = QListWidgetItem()
track_text = (
f"{track.title} - {track.artist} "
f"[{ms_to_mmss(track.duration)}] "
f"({get_relative_date(last_played)})"
)
t.setText(track_text)
t.setData(Qt.ItemDataRole.UserRole, track)
self.ui.matchList.addItem(t)
def closeEvent(self, event: Optional[QEvent]) -> None:
"""
Override close and save dialog coordinates
"""
if not event:
return
record = Settings.get_int_settings(self.session, "dbdialog_height")
if record.f_int != self.height():
record.update(self.session, {"f_int": self.height()})
record = Settings.get_int_settings(self.session, "dbdialog_width")
if record.f_int != self.width():
record.update(self.session, {"f_int": self.width()})
event.accept()
def keyPressEvent(self, event):
"""
Clear selection on ESC if there is one
"""
if event.key() == Qt.Key.Key_Escape:
if self.ui.matchList.selectedItems():
self.ui.matchList.clearSelection()
return
super(TrackSelectDialog, self).keyPressEvent(event)
def select_searchtext(self) -> None:
"""Select the searchbox"""
self.ui.searchString.selectAll()
self.ui.searchString.setFocus()
def selection_changed(self) -> None:
"""Display selected track path in dialog box"""
if not self.ui.matchList.selectedItems():
return
item = self.ui.matchList.currentItem()
track = item.data(Qt.ItemDataRole.UserRole)
last_playdate = max(track.playdates, key=lambda p: p.lastplayed, default=None)
if last_playdate:
last_played = last_playdate.lastplayed
else:
last_played = None
path_text = f"{track.path} ({get_relative_date(last_played)})"
self.ui.dbPath.setText(path_text)
def title_artist_toggle(self) -> None:
"""
Handle switching between searching for artists and searching for
titles
"""
# Logic is handled already in chars_typed(), so just call that.
self.chars_typed(self.ui.searchString.text())

View File

@ -1,6 +1,3 @@
from datetime import datetime
from email.message import EmailMessage
from typing import Any, Dict, Optional
import functools import functools
import os import os
import psutil import psutil
@ -9,18 +6,17 @@ import smtplib
import ssl import ssl
import tempfile import tempfile
from config import Config
from datetime import datetime
from email.message import EmailMessage
from log import log
from mutagen.flac import FLAC # type: ignore from mutagen.flac import FLAC # type: ignore
from mutagen.mp3 import MP3 # type: ignore from mutagen.mp3 import MP3 # type: ignore
from pydub import AudioSegment, effects from pydub import AudioSegment, effects
from pydub.utils import mediainfo from pydub.utils import mediainfo
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
from tinytag import TinyTag # type: ignore from tinytag import TinyTag # type: ignore
from typing import Any, Dict, Optional
from config import Config
from log import log
# Classes are defined after global functions so that classes can use
# those functions.
def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool: def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:

View File

@ -1,7 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from datetime import datetime, timedelta from log import log
from os.path import basename from os.path import basename
import argparse
import os
import numpy as np
import pyqtgraph as pg # type: ignore
import stackprinter # type: ignore
import subprocess
import sys
import threading
from datetime import datetime, timedelta
from pygame import mixer
from time import sleep from time import sleep
from typing import ( from typing import (
cast, cast,
@ -9,20 +20,18 @@ from typing import (
Optional, Optional,
Sequence, Sequence,
) )
import argparse
import os
import subprocess
import sys
import threading
from pygame import mixer from playlistmodel import PlaylistModel
from sqlalchemy import text
from PyQt6.QtCore import ( from PyQt6.QtCore import (
pyqtSignal, pyqtSignal,
QDate, QDate,
QEvent, QEvent,
QObject, QObject,
QSize,
Qt, Qt,
QSize,
QThread, QThread,
QTime, QTime,
QTimer, QTimer,
@ -45,39 +54,28 @@ from PyQt6.QtWidgets import (
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QMessageBox, QMessageBox,
QProgressBar,
QPushButton, QPushButton,
QProgressBar,
) )
from sqlalchemy import text
import stackprinter # type: ignore
from classes import (
CurrentTrack,
FadeCurve,
MusicMusterSignals,
NextTrack,
PlaylistTrack,
PreviousTrack,
)
from config import Config
from dbconfig import ( from dbconfig import (
engine, engine,
scoped_session,
Session, Session,
scoped_session,
) )
from dialogs import TrackSelectDialog import helpers
from log import log import icons_rc # noqa F401
import music
from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
from playlistmodel import PlaylistModel from config import Config
from datastructures import MusicMusterSignals
from playlists import PlaylistTab from playlists import PlaylistTab
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
from ui.main_window_ui import Ui_MainWindow # type: ignore from ui.main_window_ui import Ui_MainWindow # type: ignore
from utilities import check_db, update_bitrates from utilities import check_db, update_bitrates
import helpers
import icons_rc # noqa F401
import music
class CartButton(QPushButton): class CartButton(QPushButton):
@ -147,6 +145,59 @@ class CartButton(QPushButton):
self.pgb.setGeometry(0, 0, self.width(), 10) self.pgb.setGeometry(0, 0, self.width(), 10)
class FadeCurve:
GraphWidget = None
def __init__(self, track):
"""
Set up fade graph array
"""
audio = helpers.get_audio_segment(track.path)
if not audio:
return None
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence
self.start_ms = max(0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
self.end_ms = track.silence_at
self.audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
self.region = None
def clear(self) -> None:
"""Clear the current graph"""
if self.GraphWidget:
self.GraphWidget.clear()
def plot(self):
self.curve = self.GraphWidget.plot(self.graph_array)
self.curve.setPen(Config.FADE_CURVE_FOREGROUND)
def tick(self, play_time) -> None:
"""Update volume fade curve"""
if not self.GraphWidget:
return
ms_of_graph = play_time - self.start_ms
if ms_of_graph < 0:
return
if self.region is None:
# Create the region now that we're into fade
self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
self.GraphWidget.addItem(self.region)
# Update region position
self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor])
class ImportTrack(QObject): class ImportTrack(QObject):
import_error = pyqtSignal(str) import_error = pyqtSignal(str)
importing = pyqtSignal(str) importing = pyqtSignal(str)
@ -187,6 +238,84 @@ class ImportTrack(QObject):
self.finished.emit(self.playlist) self.finished.emit(self.playlist)
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
typically the previous, current and next track.
"""
def __init__(self) -> None:
"""
Only initialises data structure. Call set_plr to populate.
Do NOT store row_number here - that changes if tracks are reordered
in playlist (add, remove, drag/drop) and we shouldn't care about row
number: that's the playlist's problem.
"""
self.artist: Optional[str] = None
self.duration: Optional[int] = None
self.end_time: Optional[datetime] = None
self.fade_at: Optional[int] = None
self.fade_curve: Optional[FadeCurve] = None
self.fade_length: Optional[int] = None
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None
self.playlist_tab: Optional[PlaylistTab] = None
self.plr_id: Optional[int] = None
self.silence_at: Optional[int] = None
self.start_gap: Optional[int] = None
self.start_time: Optional[datetime] = None
self.title: Optional[str] = None
self.track_id: Optional[int] = None
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"playlist_id={self.playlist_id}>"
)
def set_plr(
self, session: scoped_session, plr: PlaylistRows, tab: PlaylistTab
) -> None:
"""
Update with new plr information
"""
if not plr.track:
return
self.playlist_tab = tab
session.add(plr)
track = plr.track
self.artist = track.artist
self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at
self.fade_graph = FadeCurve(track)
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None
self.title = track.title
self.track_id = track.id
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
def start(self) -> None:
"""
Called when track starts playing
"""
self.start_time = datetime.now()
if self.duration:
self.end_time = self.start_time + timedelta(milliseconds=self.duration)
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None, *args, **kwargs) -> None: def __init__(self, parent=None, *args, **kwargs) -> None:
super().__init__(parent) super().__init__(parent)
@ -199,9 +328,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.music: music.Music = music.Music() self.music: music.Music = music.Music()
self.playing: bool = False self.playing: bool = False
self.current_track = CurrentTrack() self.current_track = PlaylistTrack()
self.next_track = NextTrack() self.next_track = PlaylistTrack()
self.previous_track = PreviousTrack() self.previous_track = PlaylistTrack()
self.previous_track_position: Optional[float] = None self.previous_track_position: Optional[float] = None
self.selected_plrs: Optional[List[PlaylistRows]] = None self.selected_plrs: Optional[List[PlaylistRows]] = None
@ -393,7 +522,7 @@ class Window(QMainWindow, Ui_MainWindow):
Clear next track Clear next track
""" """
self.next_track = NextTrack() self.next_track = PlaylistTrack()
self.update_headers() self.update_headers()
def clear_selection(self) -> None: def clear_selection(self) -> None:
@ -470,29 +599,27 @@ class Window(QMainWindow, Ui_MainWindow):
Return True if tab closed else False. Return True if tab closed else False.
""" """
return False # Don't close current track playlist
# TODO Reimplement without ussing self.current_track.playlist_tab if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab):
# # Don't close current track playlist self.statusbar.showMessage("Can't close current track playlist", 5000)
# if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab): return False
# self.statusbar.showMessage("Can't close current track playlist", 5000)
# return False
# # Attempt to close next track playlist # Attempt to close next track playlist
# if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab: if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab:
# self.next_track.playlist_tab.clear_next() self.next_track.playlist_tab.clear_next()
# # Record playlist as closed and update remaining playlist tabs # Record playlist as closed and update remaining playlist tabs
# with Session() as session: with Session() as session:
# playlist_id = self.tabPlaylist.widget(tab_index).playlist_id playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
# playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
# if playlist: if playlist:
# playlist.close(session) playlist.close(session)
# # Close playlist and remove tab # Close playlist and remove tab
# self.tabPlaylist.widget(tab_index).close() self.tabPlaylist.widget(tab_index).close()
# self.tabPlaylist.removeTab(tab_index) self.tabPlaylist.removeTab(tab_index)
# return True return True
def connect_signals_slots(self) -> None: def connect_signals_slots(self) -> None:
self.action_About.triggered.connect(self.about) self.action_About.triggered.connect(self.about)
@ -708,17 +835,16 @@ class Window(QMainWindow, Ui_MainWindow):
self.playing = False self.playing = False
# Tell playlist_tab track has finished # Tell playlist_tab track has finished
# TODO Reimplement as a signal if self.current_track.playlist_tab:
# if self.current_track.playlist_tab: self.current_track.playlist_tab.play_ended()
# self.current_track.playlist_tab.play_ended()
# Reset fade graph # Reset fade graph
self.current_track.fade_graph.clear() self.current_track.fade_graph.clear()
# Reset PlaylistTrack objects # Reset PlaylistTrack objects
if self.current_track.track_id: if self.current_track.track_id:
self.previous_track = cast(PreviousTrack, self.current_track) self.previous_track = self.current_track
self.current_track = CurrentTrack() self.current_track = PlaylistTrack()
# Reset clocks # Reset clocks
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
@ -782,6 +908,18 @@ class Window(QMainWindow, Ui_MainWindow):
self.stop_playing(fade=True) self.stop_playing(fade=True)
def get_one_track(self, session: scoped_session) -> Optional[Tracks]:
"""Show dialog box to select one track and return it to caller"""
dlg = TrackSelectDialog(self, session)
dlg.ui.txtNote.hide()
dlg.ui.lblNote.hide()
if dlg.exec():
return dlg.track
else:
return None
def get_playtime(self) -> int: def get_playtime(self) -> int:
""" """
Return number of milliseconds current track has been playing or Return number of milliseconds current track has been playing or
@ -876,7 +1014,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.worker.finished.connect(self.import_complete) self.worker.finished.connect(self.import_complete)
self.import_thread.start() self.import_thread.start()
def import_complete(self): def import_complete(self, playlist_tab: PlaylistTab):
""" """
Called by thread when track import complete Called by thread when track import complete
""" """
@ -1165,7 +1303,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Move next track to current track. # Move next track to current track.
# stop_playing() above has called end_of_track_actions() # stop_playing() above has called end_of_track_actions()
# which will have populated self.previous_track # which will have populated self.previous_track
self.current_track = cast(CurrentTrack, self.next_track) self.current_track = self.next_track
self.clear_next() self.clear_next()
if not self.current_track.track_id: if not self.current_track.track_id:
@ -1176,10 +1314,9 @@ class Window(QMainWindow, Ui_MainWindow):
return return
# Set current track playlist_tab colour # Set current track playlist_tab colour
# TODO Reimplement without reference to self.current_track.playlist_tab current_tab = self.current_track.playlist_tab
# current_tab = self.current_track.playlist_tab if current_tab:
# if current_tab: self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
# self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
# Restore volume if -3dB active # Restore volume if -3dB active
if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
@ -1208,9 +1345,8 @@ class Window(QMainWindow, Ui_MainWindow):
Playdates(session, self.current_track.track_id) Playdates(session, self.current_track.track_id)
# Tell playlist track is now playing # Tell playlist track is now playing
# TODO Reimplement without reference to self.current_track.playlist_tab: if self.current_track.playlist_tab:
# if self.current_track.playlist_tab: self.current_track.playlist_tab.play_started(session)
# self.current_track.playlist_tab.play_started(session)
# Note that track is now playing # Note that track is now playing
self.playing = True self.playing = True
@ -1269,45 +1405,41 @@ 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
""" """
return # Return if no saved position
# TODO Reimplement without reference to playlist_tab if not self.previous_track_position:
# # Return if no saved position return
# if not self.previous_track_position:
# return
# # Note any playing track as this will become the next track # Note any playing track as this will become the next track
# playing_track = None playing_track = None
# if self.current_track.track_id: if self.current_track.track_id:
# playing_track = self.current_track playing_track = self.current_track
# # Set next plr to be track to resume # Set next plr to be track to resume
# if not self.previous_track.plr_id: if not self.previous_track.plr_id:
# return return
# # TODO Reimplement following two lines if not self.previous_track.playlist_tab:
# # if not self.previous_track.playlist_tab: return
# # return
# # Resume last track # Resume last track
# # TODO Reimplement next four lines self.set_next_plr_id(
# # self.set_next_plr_id( self.previous_track.plr_id, self.previous_track.playlist_tab
# # self.previous_track.plr_id, self.previous_track.playlist_tab )
# # ) self.play_next(self.previous_track_position)
# # self.play_next(self.previous_track_position)
# # Adjust track info so that clocks and graph are correct. # Adjust track info so that clocks and graph are correct.
# # Easiest way is to fake the start time. # Easiest way is to fake the start time.
# if self.current_track.start_time and self.current_track.duration: if self.current_track.start_time and self.current_track.duration:
# elapsed_ms = self.current_track.duration * self.previous_track_position elapsed_ms = self.current_track.duration * self.previous_track_position
# self.current_track.start_time -= timedelta(milliseconds=elapsed_ms) self.current_track.start_time -= timedelta(milliseconds=elapsed_ms)
# # If a track was playing when we were called, get details to # If a track was playing when we were called, get details to
# # set it as the next track # set it as the next track
# if playing_track: if playing_track:
# if not playing_track.plr_id: if not playing_track.plr_id:
# return return
# if not playing_track.playlist_tab: if not playing_track.playlist_tab:
# return return
# self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab) self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab)
def save_as_template(self) -> None: def save_as_template(self) -> None:
"""Save current playlist as template""" """Save current playlist as template"""
@ -1433,20 +1565,16 @@ class Window(QMainWindow, Ui_MainWindow):
def show_current(self) -> None: def show_current(self) -> None:
"""Scroll to show current track""" """Scroll to show current track"""
return if self.current_track.playlist_tab != self.active_tab():
# TODO Reimplement self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab)
# if self.current_track.playlist_tab != self.active_tab(): self.tabPlaylist.currentWidget().scroll_current_to_top()
# self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab)
# self.tabPlaylist.currentWidget().scroll_current_to_top()
def show_next(self) -> None: def show_next(self) -> None:
"""Scroll to show next track""" """Scroll to show next track"""
return if self.next_track.playlist_tab != self.active_tab():
# TODO Reimplement self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
# if self.next_track.playlist_tab != self.active_tab(): self.tabPlaylist.currentWidget().scroll_next_to_top()
# self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
# self.tabPlaylist.currentWidget().scroll_next_to_top()
def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]: def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]:
"""Get name of playlist from user""" """Get name of playlist from user"""
@ -1491,16 +1619,15 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.stop() self.music.stop()
# Reset playlist_tab colour # Reset playlist_tab colour
# TODO Reimplement if self.current_track.playlist_tab:
# if self.current_track.playlist_tab: if self.current_track.playlist_tab == self.next_track.playlist_tab:
# if self.current_track.playlist_tab == self.next_track.playlist_tab: self.set_tab_colour(
# self.set_tab_colour( self.current_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
# self.current_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB) )
# ) else:
# else: self.set_tab_colour(
# self.set_tab_colour( self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
# self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB) )
# )
# Run end-of-track actions # Run end-of-track actions
self.end_of_track_actions() self.end_of_track_actions()
@ -1544,11 +1671,11 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
# Update self.next_track PlaylistTrack structure # Update self.next_track PlaylistTrack structure
old_next_track = self.next_track old_next_track = self.next_track
self.next_track = NextTrack() self.next_track = PlaylistTrack()
if next_plr_id: if next_plr_id:
next_plr = session.get(PlaylistRows, next_plr_id) next_plr = session.get(PlaylistRows, next_plr_id)
if next_plr: if next_plr:
self.next_track.set_plr(session, next_plr) self.next_track.set_plr(session, next_plr, playlist_tab)
# Tell playlist tabs to update their 'next track' highlighting # Tell playlist tabs to update their 'next track' highlighting
# Args must both be ints, so use zero for no next track # Args must both be ints, so use zero for no next track
@ -1583,32 +1710,30 @@ class Window(QMainWindow, Ui_MainWindow):
# If the original next playlist tab isn't the same as the # If the original next playlist tab isn't the same as the
# new one or the current track, it needs its colour reset. # new one or the current track, it needs its colour reset.
return if (
# TODO Reimplement old_next_track
# if ( and old_next_track.playlist_tab
# old_next_track and old_next_track.playlist_tab
# and old_next_track.playlist_tab not in [self.next_track.playlist_tab, self.current_track.playlist_tab]
# and old_next_track.playlist_tab ):
# not in [self.next_track.playlist_tab, self.current_track.playlist_tab] self.set_tab_colour(
# ): old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
# self.set_tab_colour( )
# old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB) # If the new next playlist tab isn't the same as the
# ) # old one or the current track, it needs its colour set.
# # If the new next playlist tab isn't the same as the if old_next_track:
# # old one or the current track, it needs its colour set. old_tab = old_next_track.playlist_tab
# if old_next_track: else:
# old_tab = old_next_track.playlist_tab old_tab = None
# else: if (
# old_tab = None self.next_track
# if ( and self.next_track.playlist_tab
# self.next_track and self.next_track.playlist_tab
# and self.next_track.playlist_tab not in [old_tab, self.current_track.playlist_tab]
# and self.next_track.playlist_tab ):
# not in [old_tab, self.current_track.playlist_tab] self.set_tab_colour(
# ): self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
# self.set_tab_colour( )
# self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
# )
def tick_10ms(self) -> None: def tick_10ms(self) -> None:
""" """
@ -1776,6 +1901,164 @@ class CartDialog(QDialog):
self.ui.lblPath.setText(self.path) self.ui.lblPath.setText(self.path)
class TrackSelectDialog(QDialog):
"""Select track from database"""
def __init__(
self,
session: scoped_session,
new_row_number: int,
playlist_id: int,
*args,
**kwargs,
) -> None:
"""
Subclassed QDialog to manage track selection
"""
super().__init__(*args, **kwargs)
self.session = session
self.new_row_number = new_row_number
self.playlist_id = playlist_id
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.ui.btnAdd.clicked.connect(self.add_selected)
self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)
self.ui.btnClose.clicked.connect(self.close)
self.ui.matchList.itemDoubleClicked.connect(self.add_selected)
self.ui.matchList.itemSelectionChanged.connect(self.selection_changed)
self.ui.radioTitle.toggled.connect(self.title_artist_toggle)
self.ui.searchString.textEdited.connect(self.chars_typed)
self.track: Optional[Tracks] = None
self.signals = MusicMusterSignals()
record = Settings.get_int_settings(self.session, "dbdialog_width")
width = record.f_int or 800
record = Settings.get_int_settings(self.session, "dbdialog_height")
height = record.f_int or 600
self.resize(width, height)
def add_selected(self) -> None:
"""Handle Add button"""
track = None
if self.ui.matchList.selectedItems():
item = self.ui.matchList.currentItem()
if item:
track = item.data(Qt.ItemDataRole.UserRole)
note = self.ui.txtNote.text()
if not note and not track:
return
self.ui.txtNote.clear()
self.select_searchtext()
track_id = None
if track:
track_id = track.id
self.signals.add_track_to_playlist_signal.emit(
self.playlist_id, self.new_row_number, track_id, note
)
def add_selected_and_close(self) -> None:
"""Handle Add and Close button"""
self.add_selected()
self.accept()
def chars_typed(self, s: str) -> None:
"""Handle text typed in search box"""
self.ui.matchList.clear()
if len(s) > 0:
if self.ui.radioTitle.isChecked():
matches = Tracks.search_titles(self.session, "%" + s)
else:
matches = Tracks.search_artists(self.session, "%" + s)
if matches:
for track in matches:
last_played = None
last_playdate = max(
track.playdates, key=lambda p: p.lastplayed, default=None
)
if last_playdate:
last_played = last_playdate.lastplayed
t = QListWidgetItem()
track_text = (
f"{track.title} - {track.artist} "
f"[{helpers.ms_to_mmss(track.duration)}] "
f"({helpers.get_relative_date(last_played)})"
)
t.setText(track_text)
t.setData(Qt.ItemDataRole.UserRole, track)
self.ui.matchList.addItem(t)
def closeEvent(self, event: Optional[QEvent]) -> None:
"""
Override close and save dialog coordinates
"""
if not event:
return
record = Settings.get_int_settings(self.session, "dbdialog_height")
if record.f_int != self.height():
record.update(self.session, {"f_int": self.height()})
record = Settings.get_int_settings(self.session, "dbdialog_width")
if record.f_int != self.width():
record.update(self.session, {"f_int": self.width()})
event.accept()
def keyPressEvent(self, event):
"""
Clear selection on ESC if there is one
"""
if event.key() == Qt.Key.Key_Escape:
if self.ui.matchList.selectedItems():
self.ui.matchList.clearSelection()
return
super(TrackSelectDialog, self).keyPressEvent(event)
def select_searchtext(self) -> None:
"""Select the searchbox"""
self.ui.searchString.selectAll()
self.ui.searchString.setFocus()
def selection_changed(self) -> None:
"""Display selected track path in dialog box"""
if not self.ui.matchList.selectedItems():
return
item = self.ui.matchList.currentItem()
track = item.data(Qt.ItemDataRole.UserRole)
last_playdate = max(track.playdates, key=lambda p: p.lastplayed, default=None)
if last_playdate:
last_played = last_playdate.lastplayed
else:
last_played = None
path_text = f"{track.path} ({helpers.get_relative_date(last_played)})"
self.ui.dbPath.setText(path_text)
def title_artist_toggle(self) -> None:
"""
Handle switching between searching for artists and searching for
titles
"""
# Logic is handled already in chars_typed(), so just call that.
self.chars_typed(self.ui.searchString.text())
class DownloadCSV(QDialog): class DownloadCSV(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__() super().__init__()

View File

@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from enum import auto, Enum from enum import auto, Enum
from typing import List, Optional from sqlalchemy import bindparam, update
from typing import List, Optional, TYPE_CHECKING
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QAbstractTableModel, QAbstractTableModel,
@ -14,11 +15,12 @@ from PyQt6.QtGui import (
QFont, QFont,
) )
from classes import CurrentTrack, MusicMusterSignals, NextTrack
from config import Config from config import Config
from datastructures import MusicMusterSignals
from dbconfig import scoped_session, Session from dbconfig import scoped_session, Session
from helpers import file_is_unreadable from helpers import (
from log import log file_is_unreadable,
)
from models import PlaylistRows, Tracks from models import PlaylistRows, Tracks
@ -102,7 +104,6 @@ class PlaylistModel(QAbstractTableModel):
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.signals.add_track_to_playlist_signal.connect(self.add_track) self.signals.add_track_to_playlist_signal.connect(self.add_track)
self.signals.add_track_to_header_signal.connect(self.add_track_to_header)
with Session() as session: with Session() as session:
self.refresh_data(session) self.refresh_data(session)
@ -137,49 +138,6 @@ class PlaylistModel(QAbstractTableModel):
# No track, no note, no point # No track, no note, no point
return return
def add_track_to_header(
self,
playlist_id: int,
row_number: int,
track_id: int,
) -> None:
"""
Add track to existing header row if it's for our playlist
"""
# Ignore if it's not for us
if playlist_id != self.playlist_id:
return
# Get existing row
try:
prd = self.playlist_rows[row_number]
except KeyError:
log.error(
f"KeyError in PlaylistModel:add_track_to_header ({playlist_id=}, "
f"{row_number=}, {track_id=}, {len(self.playlist_rows)=}"
)
return
if prd.path:
log.error(
f"Error in PlaylistModel:add_track_to_header ({prd=}, "
"Header row already has track associated"
)
return
with Session() as session:
plr = session.get(PlaylistRows, prd.plrid)
if plr:
# Add track to PlaylistRows
plr.track_id = track_id
# Reset header row spanning
self.signals.span_cells_signal.emit(
row_number, HEADER_NOTES_COLUMN, 1, 1
)
# Update local copy
self.refresh_row(session, row_number)
# Repaint row
self.invalidate_row(row_number)
def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush: def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush:
"""Return background setting""" """Return background setting"""
@ -363,13 +321,6 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
def is_header_row(self, row_number: int) -> bool:
"""
Return True if row is a header row, else False
"""
return self.playlist_rows[row_number].path == ""
def insert_header_row(self, row_number: Optional[int], text: str) -> None: def insert_header_row(self, row_number: Optional[int], text: str) -> None:
""" """
Insert a header row. Insert a header row.

View File

@ -7,7 +7,7 @@ import threading
import obsws_python as obs # type: ignore import obsws_python as obs # type: ignore
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Callable, cast, List, Optional, Tuple, TYPE_CHECKING from typing import Any, cast, List, Optional, Tuple, TYPE_CHECKING
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QEvent, QEvent,
@ -22,7 +22,7 @@ from PyQt6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QHeaderView, QHeaderView,
QMenu, # QMenu,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
QStyledItemDelegate, QStyledItemDelegate,
@ -35,9 +35,8 @@ from PyQt6.QtWidgets import (
QStyleOption, QStyleOption,
) )
from datastructures import MusicMusterSignals
from dbconfig import Session, scoped_session from dbconfig import Session, scoped_session
from dialogs import TrackSelectDialog
from classes import MusicMusterSignals
from config import Config from config import Config
from helpers import ( from helpers import (
ask_yes_no, ask_yes_no,
@ -49,11 +48,11 @@ from helpers import (
set_track_metadata, set_track_metadata,
) )
from log import log from log import log
from models import PlaylistRows, Settings, Tracks, NoteColours from models import Playlists, PlaylistRows, Settings, Tracks, NoteColours
from playlistmodel import PlaylistModel
if TYPE_CHECKING: if TYPE_CHECKING:
from musicmuster import Window from musicmuster import Window, MusicMusterSignals
from playlistmodel import PlaylistModel
HEADER_NOTES_COLUMN = 2 HEADER_NOTES_COLUMN = 2
@ -79,8 +78,7 @@ class EscapeDelegate(QStyledItemDelegate):
Intercept createEditor call and make row just a little bit taller Intercept createEditor call and make row just a little bit taller
""" """
self.signals = MusicMusterSignals() signals.enable_escape_signal.emit(False)
self.signals.enable_escape_signal.emit(False)
if isinstance(self.parent(), PlaylistTab): if isinstance(self.parent(), PlaylistTab):
p = cast(PlaylistTab, self.parent()) p = cast(PlaylistTab, self.parent())
if isinstance(index.data(), str): if isinstance(index.data(), str):
@ -113,7 +111,7 @@ class EscapeDelegate(QStyledItemDelegate):
return True return True
elif key_event.key() == Qt.Key.Key_Escape: elif key_event.key() == Qt.Key.Key_Escape:
discard_edits = QMessageBox.question( discard_edits = QMessageBox.question(
cast(QWidget, self), "Abandon edit", "Discard changes?" self.parent(), "Abandon edit", "Discard changes?"
) )
if discard_edits == QMessageBox.StandardButton.Yes: if discard_edits == QMessageBox.StandardButton.Yes:
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
@ -136,7 +134,8 @@ class PlaylistStyle(QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None): def drawPrimitive(self, element, option, painter, widget=None):
""" """
Draw a line across the entire row rather than just the column Draw a line across the entire row rather than just the column
we're hovering over. we're hovering over. This may not always work depending on global
style - for instance I think it won't work on OSX.
""" """
if ( if (
element == QStyle.PrimitiveElement.PE_IndicatorItemViewItemDrop element == QStyle.PrimitiveElement.PE_IndicatorItemViewItemDrop
@ -179,9 +178,9 @@ class PlaylistTab(QTableView):
# rows selected # rows selected
self.setDragEnabled(True) self.setDragEnabled(True)
# Prepare for context menu # Prepare for context menu
self.menu = QMenu() # self.menu = QMenu()
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._context_menu) # self.customContextMenuRequested.connect(self._context_menu)
# Connect signals # Connect signals
# This dancing is to satisfy mypy # This dancing is to satisfy mypy
@ -292,27 +291,27 @@ class PlaylistTab(QTableView):
# self.hide_or_show_played_tracks() # self.hide_or_show_played_tracks()
def _add_context_menu( # def _add_context_menu(
self, # self,
text: str, # text: str,
action: Callable, # action: Callable,
disabled: bool = False, # disabled: bool = False,
parent_menu: Optional[QMenu] = None, # parent_menu: Optional[QMenu] = None,
) -> Optional[QAction]: # ) -> Optional[QAction]:
""" # """
Add item to self.menu # Add item to self.menu
""" # """
if parent_menu is None: # if parent_menu is None:
parent_menu = self.menu # parent_menu = self.menu
menu_item = parent_menu.addAction(text) # menu_item = parent_menu.addAction(text)
if not menu_item: # if not menu_item:
return None # return None
menu_item.setDisabled(disabled) # menu_item.setDisabled(disabled)
menu_item.triggered.connect(action) # menu_item.triggered.connect(action)
return menu_item # return menu_item
# def mouseReleaseEvent(self, event): # def mouseReleaseEvent(self, event):
# """ # """
@ -1034,90 +1033,106 @@ class PlaylistTab(QTableView):
"""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"""
with Session() as session: with Session() as session:
dlg = TrackSelectDialog( # Add track to playlist row
session=session, plr = self._get_row_plr(session, row_number)
new_row_number=row_number, if not plr:
playlist_id=self.playlist_id, return
add_to_header=True,
)
dlg.exec()
def _build_context_menu(self, item: QTableWidgetItem) -> None: # Don't add track if there's already a track there
"""Used to process context (right-click) menu, which is defined here""" if plr.track_id is not None:
return
self.menu.clear() # Get track
row_number = item.row() track = self.musicmuster.get_one_track(session)
# track_id = self._get_row_track_id(row_number) if not track:
# track_row = bool(track_id) return
header_row = False plr.track_id = track.id
model = cast(PlaylistModel, self.model())
if model:
header_row = model.is_header_row(row_number) # Reset row span
# current = row_number == self._get_current_track_row_number() self.setSpan(row_number, HEADER_NOTES_COLUMN, 1, 1)
# next_row = row_number == self._get_next_track_row_number()
# # Play with mplayer # Update attributes of row
# if track_row and not current: self._update_row_track_info(session, row_number, track)
# self._add_context_menu( self._set_row_bold(row_number)
# "Play with mplayer", lambda: self._mplayer_play(row_number) self._set_row_colour_default(row_number)
# ) self._set_row_note_text(session, row_number, plr.note)
self.clear_selection()
self.save_playlist(session)
# Update times once display updated
self._update_start_end_times(session)
# # Paste # def _build_context_menu(self, item: QTableWidgetItem) -> None:
# self._add_context_menu( # """Used to process context (right-click) menu, which is defined here"""
# "Paste",
# lambda: self.musicmuster.paste_rows(),
# self.musicmuster.selected_plrs is None,
# )
# # Open in Audacity # self.menu.clear()
# if track_row and not current: # row_number = item.row()
# self._add_context_menu( # track_id = self._get_row_track_id(row_number)
# "Open in Audacity", lambda: self._open_in_audacity(row_number) # track_row = bool(track_id)
# ) # header_row = not track_row
# current = row_number == self._get_current_track_row_number()
# next_row = row_number == self._get_next_track_row_number()
# # Rescan # # Play with mplayer
# if track_row and not current: # if track_row and not current:
# self._add_context_menu( # self._add_context_menu(
# "Rescan track", lambda: self._rescan(row_number, track_id) # "Play with mplayer", lambda: self._mplayer_play(row_number)
# ) # )
# # ---------------------- # # Paste
self.menu.addSeparator() # self._add_context_menu(
# "Paste",
# lambda: self.musicmuster.paste_rows(),
# self.musicmuster.selected_plrs is None,
# )
# # Remove row # # Open in Audacity
# if not current and not next_row: # if track_row and not current:
# self._add_context_menu("Delete row", self._delete_rows) # self._add_context_menu(
# "Open in Audacity", lambda: self._open_in_audacity(row_number)
# )
# # Move to playlist # # Rescan
# if not current and not next_row: # if track_row and not current:
# self._add_context_menu( # self._add_context_menu(
# "Move to playlist...", self.musicmuster.move_selected # "Rescan track", lambda: self._rescan(row_number, track_id)
# ) # )
# # ---------------------- # # ----------------------
# self.menu.addSeparator() # self.menu.addSeparator()
# # Remove track from row # # Remove row
# if track_row and not current and not next_row: # if not current and not next_row:
# self._add_context_menu( # self._add_context_menu("Delete row", self._delete_rows)
# "Remove track from row", lambda: self._remove_track(row_number)
# )
# Add track to section header (ie, make this a track row) # # Move to playlist
if header_row: # if not current and not next_row:
self._add_context_menu("Add a track", lambda: self._add_track(row_number)) # self._add_context_menu(
# "Move to playlist...", self.musicmuster.move_selected
# )
# # Mark unplayed # # ----------------------
# if self._get_row_userdata(row_number, self.PLAYED): # self.menu.addSeparator()
# self._add_context_menu("Mark unplayed", self._mark_unplayed)
# # Unmark as next # # Remove track from row
# if next_row: # if track_row and not current and not next_row:
# self._add_context_menu("Unmark as next track", self.clear_next) # self._add_context_menu(
# "Remove track from row", lambda: self._remove_track(row_number)
# )
# # ---------------------- # # Add track to section header (ie, make this a track row)
self.menu.addSeparator() # if header_row:
# self._add_context_menu("Add a track", lambda: self._add_track(row_number))
# # Mark unplayed
# if self._get_row_userdata(row_number, self.PLAYED):
# self._add_context_menu("Mark unplayed", self._mark_unplayed)
# # Unmark as next
# if next_row:
# self._add_context_menu("Unmark as next track", self.clear_next)
# # ----------------------
# self.menu.addSeparator()
# # Sort # # Sort
# sort_menu = self.menu.addMenu("Sort") # sort_menu = self.menu.addMenu("Sort")
@ -1183,12 +1198,12 @@ class PlaylistTab(QTableView):
record = Settings.get_int_settings(session, attr_name) record = Settings.get_int_settings(session, attr_name)
record.f_int = self.columnWidth(column_number) record.f_int = self.columnWidth(column_number)
def _context_menu(self, pos): # def _context_menu(self, pos):
"""Display right-click menu""" # """Display right-click menu"""
item = self.indexAt(pos) # item = self.itemAt(pos)
self._build_context_menu(item) # self._build_context_menu(item)
self.menu.exec(self.mapToGlobal(pos)) # self.menu.exec(self.mapToGlobal(pos))
def _copy_path(self, row_number: int) -> None: def _copy_path(self, row_number: int) -> None:
""" """