Compare commits
No commits in common. "d9ad001c75766bc700c42c9331a2a81b65979172" and "955433686041fa30e1efd43bd6346caa967d9648" have entirely different histories.
d9ad001c75
...
9554336860
176
app/classes.py
176
app/classes.py
@ -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
20
app/datastructures.py
Normal 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)
|
||||||
178
app/dialogs.py
178
app/dialogs.py
@ -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())
|
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Don't close current track playlist
|
||||||
|
if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab):
|
||||||
|
self.statusbar.showMessage("Can't close current track playlist", 5000)
|
||||||
return False
|
return False
|
||||||
# TODO Reimplement without ussing self.current_track.playlist_tab
|
|
||||||
# # Don't close current track playlist
|
|
||||||
# if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab):
|
|
||||||
# self.statusbar.showMessage("Can't close current track playlist", 5000)
|
|
||||||
# return False
|
|
||||||
|
|
||||||
# # 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 if no saved position
|
||||||
|
if not self.previous_track_position:
|
||||||
return
|
return
|
||||||
# TODO Reimplement without reference to playlist_tab
|
|
||||||
# # Return if no saved position
|
|
||||||
# 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__()
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
131
app/playlists.py
131
app/playlists.py
@ -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,26 +1033,42 @@ 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)
|
||||||
|
if not track:
|
||||||
|
return
|
||||||
|
plr.track_id = track.id
|
||||||
|
|
||||||
|
# Reset row span
|
||||||
|
self.setSpan(row_number, HEADER_NOTES_COLUMN, 1, 1)
|
||||||
|
|
||||||
|
# Update attributes of row
|
||||||
|
self._update_row_track_info(session, row_number, track)
|
||||||
|
self._set_row_bold(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)
|
||||||
|
|
||||||
|
# def _build_context_menu(self, item: QTableWidgetItem) -> None:
|
||||||
|
# """Used to process context (right-click) menu, which is defined here"""
|
||||||
|
|
||||||
|
# self.menu.clear()
|
||||||
|
# row_number = item.row()
|
||||||
# track_id = self._get_row_track_id(row_number)
|
# track_id = self._get_row_track_id(row_number)
|
||||||
# track_row = bool(track_id)
|
# track_row = bool(track_id)
|
||||||
header_row = False
|
# header_row = not track_row
|
||||||
model = cast(PlaylistModel, self.model())
|
|
||||||
if model:
|
|
||||||
|
|
||||||
header_row = model.is_header_row(row_number)
|
|
||||||
# current = row_number == self._get_current_track_row_number()
|
# current = row_number == self._get_current_track_row_number()
|
||||||
# next_row = row_number == self._get_next_track_row_number()
|
# next_row = row_number == self._get_next_track_row_number()
|
||||||
|
|
||||||
@ -1083,7 +1098,7 @@ class PlaylistTab(QTableView):
|
|||||||
# )
|
# )
|
||||||
|
|
||||||
# # ----------------------
|
# # ----------------------
|
||||||
self.menu.addSeparator()
|
# self.menu.addSeparator()
|
||||||
|
|
||||||
# # Remove row
|
# # Remove row
|
||||||
# if not current and not next_row:
|
# if not current and not next_row:
|
||||||
@ -1104,9 +1119,9 @@ class PlaylistTab(QTableView):
|
|||||||
# "Remove track from row", lambda: self._remove_track(row_number)
|
# "Remove track from row", lambda: self._remove_track(row_number)
|
||||||
# )
|
# )
|
||||||
|
|
||||||
# Add track to section header (ie, make this a track row)
|
# # Add track to section header (ie, make this a track row)
|
||||||
if header_row:
|
# if header_row:
|
||||||
self._add_context_menu("Add a track", lambda: self._add_track(row_number))
|
# self._add_context_menu("Add a track", lambda: self._add_track(row_number))
|
||||||
|
|
||||||
# # Mark unplayed
|
# # Mark unplayed
|
||||||
# if self._get_row_userdata(row_number, self.PLAYED):
|
# if self._get_row_userdata(row_number, self.PLAYED):
|
||||||
@ -1117,7 +1132,7 @@ class PlaylistTab(QTableView):
|
|||||||
# self._add_context_menu("Unmark as next track", self.clear_next)
|
# self._add_context_menu("Unmark as next track", self.clear_next)
|
||||||
|
|
||||||
# # ----------------------
|
# # ----------------------
|
||||||
self.menu.addSeparator()
|
# 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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user