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 os
|
||||
import psutil
|
||||
@ -9,18 +6,17 @@ import smtplib
|
||||
import ssl
|
||||
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.mp3 import MP3 # type: ignore
|
||||
from pydub import AudioSegment, effects
|
||||
from pydub.utils import mediainfo
|
||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
|
||||
from tinytag import TinyTag # type: ignore
|
||||
|
||||
from config import Config
|
||||
from log import log
|
||||
|
||||
# Classes are defined after global functions so that classes can use
|
||||
# those functions.
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool:
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from log import log
|
||||
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 typing import (
|
||||
cast,
|
||||
@ -9,20 +20,18 @@ from typing import (
|
||||
Optional,
|
||||
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 (
|
||||
pyqtSignal,
|
||||
QDate,
|
||||
QEvent,
|
||||
QObject,
|
||||
QSize,
|
||||
Qt,
|
||||
QSize,
|
||||
QThread,
|
||||
QTime,
|
||||
QTimer,
|
||||
@ -45,39 +54,28 @@ from PyQt6.QtWidgets import (
|
||||
QListWidgetItem,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QProgressBar,
|
||||
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 (
|
||||
engine,
|
||||
scoped_session,
|
||||
Session,
|
||||
scoped_session,
|
||||
)
|
||||
from dialogs import TrackSelectDialog
|
||||
from log import log
|
||||
import helpers
|
||||
import icons_rc # noqa F401
|
||||
import music
|
||||
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 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.downloadcsv_ui import Ui_DateSelect # type: ignore
|
||||
from ui.main_window_ui import Ui_MainWindow # type: ignore
|
||||
from utilities import check_db, update_bitrates
|
||||
import helpers
|
||||
import icons_rc # noqa F401
|
||||
import music
|
||||
|
||||
|
||||
class CartButton(QPushButton):
|
||||
@ -147,6 +145,59 @@ class CartButton(QPushButton):
|
||||
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):
|
||||
import_error = pyqtSignal(str)
|
||||
importing = pyqtSignal(str)
|
||||
@ -187,6 +238,84 @@ class ImportTrack(QObject):
|
||||
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):
|
||||
def __init__(self, parent=None, *args, **kwargs) -> None:
|
||||
super().__init__(parent)
|
||||
@ -199,9 +328,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.music: music.Music = music.Music()
|
||||
self.playing: bool = False
|
||||
|
||||
self.current_track = CurrentTrack()
|
||||
self.next_track = NextTrack()
|
||||
self.previous_track = PreviousTrack()
|
||||
self.current_track = PlaylistTrack()
|
||||
self.next_track = PlaylistTrack()
|
||||
self.previous_track = PlaylistTrack()
|
||||
|
||||
self.previous_track_position: Optional[float] = None
|
||||
self.selected_plrs: Optional[List[PlaylistRows]] = None
|
||||
@ -393,7 +522,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
Clear next track
|
||||
"""
|
||||
|
||||
self.next_track = NextTrack()
|
||||
self.next_track = PlaylistTrack()
|
||||
self.update_headers()
|
||||
|
||||
def clear_selection(self) -> None:
|
||||
@ -470,29 +599,27 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
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
|
||||
# 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
|
||||
# if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab:
|
||||
# self.next_track.playlist_tab.clear_next()
|
||||
# Attempt to close next track playlist
|
||||
if self.tabPlaylist.widget(tab_index) == self.next_track.playlist_tab:
|
||||
self.next_track.playlist_tab.clear_next()
|
||||
|
||||
# # Record playlist as closed and update remaining playlist tabs
|
||||
# with Session() as session:
|
||||
# playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
|
||||
# playlist = session.get(Playlists, playlist_id)
|
||||
# if playlist:
|
||||
# playlist.close(session)
|
||||
# Record playlist as closed and update remaining playlist tabs
|
||||
with Session() as session:
|
||||
playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
playlist.close(session)
|
||||
|
||||
# # Close playlist and remove tab
|
||||
# self.tabPlaylist.widget(tab_index).close()
|
||||
# self.tabPlaylist.removeTab(tab_index)
|
||||
# Close playlist and remove tab
|
||||
self.tabPlaylist.widget(tab_index).close()
|
||||
self.tabPlaylist.removeTab(tab_index)
|
||||
|
||||
# return True
|
||||
return True
|
||||
|
||||
def connect_signals_slots(self) -> None:
|
||||
self.action_About.triggered.connect(self.about)
|
||||
@ -708,17 +835,16 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.playing = False
|
||||
|
||||
# Tell playlist_tab track has finished
|
||||
# TODO Reimplement as a signal
|
||||
# if self.current_track.playlist_tab:
|
||||
# self.current_track.playlist_tab.play_ended()
|
||||
if self.current_track.playlist_tab:
|
||||
self.current_track.playlist_tab.play_ended()
|
||||
|
||||
# Reset fade graph
|
||||
self.current_track.fade_graph.clear()
|
||||
|
||||
# Reset PlaylistTrack objects
|
||||
if self.current_track.track_id:
|
||||
self.previous_track = cast(PreviousTrack, self.current_track)
|
||||
self.current_track = CurrentTrack()
|
||||
self.previous_track = self.current_track
|
||||
self.current_track = PlaylistTrack()
|
||||
|
||||
# Reset clocks
|
||||
self.frame_fade.setStyleSheet("")
|
||||
@ -782,6 +908,18 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
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:
|
||||
"""
|
||||
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.import_thread.start()
|
||||
|
||||
def import_complete(self):
|
||||
def import_complete(self, playlist_tab: PlaylistTab):
|
||||
"""
|
||||
Called by thread when track import complete
|
||||
"""
|
||||
@ -1165,7 +1303,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# Move next track to current track.
|
||||
# stop_playing() above has called end_of_track_actions()
|
||||
# which will have populated self.previous_track
|
||||
self.current_track = cast(CurrentTrack, self.next_track)
|
||||
self.current_track = self.next_track
|
||||
self.clear_next()
|
||||
|
||||
if not self.current_track.track_id:
|
||||
@ -1176,10 +1314,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
|
||||
# Set current track playlist_tab colour
|
||||
# TODO Reimplement without reference to self.current_track.playlist_tab
|
||||
# current_tab = self.current_track.playlist_tab
|
||||
# if current_tab:
|
||||
# self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
|
||||
current_tab = self.current_track.playlist_tab
|
||||
if current_tab:
|
||||
self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
|
||||
|
||||
# Restore volume if -3dB active
|
||||
if self.btnDrop3db.isChecked():
|
||||
@ -1208,9 +1345,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
Playdates(session, self.current_track.track_id)
|
||||
|
||||
# Tell playlist track is now playing
|
||||
# TODO Reimplement without reference to self.current_track.playlist_tab:
|
||||
# if self.current_track.playlist_tab:
|
||||
# self.current_track.playlist_tab.play_started(session)
|
||||
if self.current_track.playlist_tab:
|
||||
self.current_track.playlist_tab.play_started(session)
|
||||
|
||||
# Note that track is now playing
|
||||
self.playing = True
|
||||
@ -1269,45 +1405,41 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
- If a track is playing, make that the next track
|
||||
"""
|
||||
|
||||
# Return if no saved position
|
||||
if not self.previous_track_position:
|
||||
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
|
||||
# playing_track = None
|
||||
# if self.current_track.track_id:
|
||||
# playing_track = self.current_track
|
||||
# Note any playing track as this will become the next track
|
||||
playing_track = None
|
||||
if self.current_track.track_id:
|
||||
playing_track = self.current_track
|
||||
|
||||
# # Set next plr to be track to resume
|
||||
# if not self.previous_track.plr_id:
|
||||
# return
|
||||
# # TODO Reimplement following two lines
|
||||
# # if not self.previous_track.playlist_tab:
|
||||
# # return
|
||||
# Set next plr to be track to resume
|
||||
if not self.previous_track.plr_id:
|
||||
return
|
||||
if not self.previous_track.playlist_tab:
|
||||
return
|
||||
|
||||
# # Resume last track
|
||||
# # TODO Reimplement next four lines
|
||||
# # self.set_next_plr_id(
|
||||
# # self.previous_track.plr_id, self.previous_track.playlist_tab
|
||||
# # )
|
||||
# # self.play_next(self.previous_track_position)
|
||||
# Resume last track
|
||||
self.set_next_plr_id(
|
||||
self.previous_track.plr_id, self.previous_track.playlist_tab
|
||||
)
|
||||
self.play_next(self.previous_track_position)
|
||||
|
||||
# # Adjust track info so that clocks and graph are correct.
|
||||
# # Easiest way is to fake the start time.
|
||||
# if self.current_track.start_time and self.current_track.duration:
|
||||
# elapsed_ms = self.current_track.duration * self.previous_track_position
|
||||
# self.current_track.start_time -= timedelta(milliseconds=elapsed_ms)
|
||||
# Adjust track info so that clocks and graph are correct.
|
||||
# Easiest way is to fake the start time.
|
||||
if self.current_track.start_time and self.current_track.duration:
|
||||
elapsed_ms = self.current_track.duration * self.previous_track_position
|
||||
self.current_track.start_time -= timedelta(milliseconds=elapsed_ms)
|
||||
|
||||
# # If a track was playing when we were called, get details to
|
||||
# # set it as the next track
|
||||
# if playing_track:
|
||||
# if not playing_track.plr_id:
|
||||
# return
|
||||
# if not playing_track.playlist_tab:
|
||||
# return
|
||||
# self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab)
|
||||
# If a track was playing when we were called, get details to
|
||||
# set it as the next track
|
||||
if playing_track:
|
||||
if not playing_track.plr_id:
|
||||
return
|
||||
if not playing_track.playlist_tab:
|
||||
return
|
||||
self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab)
|
||||
|
||||
def save_as_template(self) -> None:
|
||||
"""Save current playlist as template"""
|
||||
@ -1433,20 +1565,16 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def show_current(self) -> None:
|
||||
"""Scroll to show current track"""
|
||||
|
||||
return
|
||||
# TODO Reimplement
|
||||
# if self.current_track.playlist_tab != self.active_tab():
|
||||
# self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab)
|
||||
# self.tabPlaylist.currentWidget().scroll_current_to_top()
|
||||
if self.current_track.playlist_tab != self.active_tab():
|
||||
self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab)
|
||||
self.tabPlaylist.currentWidget().scroll_current_to_top()
|
||||
|
||||
def show_next(self) -> None:
|
||||
"""Scroll to show next track"""
|
||||
|
||||
return
|
||||
# TODO Reimplement
|
||||
# if self.next_track.playlist_tab != self.active_tab():
|
||||
# self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
|
||||
# self.tabPlaylist.currentWidget().scroll_next_to_top()
|
||||
if self.next_track.playlist_tab != self.active_tab():
|
||||
self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
|
||||
self.tabPlaylist.currentWidget().scroll_next_to_top()
|
||||
|
||||
def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]:
|
||||
"""Get name of playlist from user"""
|
||||
@ -1491,16 +1619,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.music.stop()
|
||||
|
||||
# Reset playlist_tab colour
|
||||
# TODO Reimplement
|
||||
# if self.current_track.playlist_tab:
|
||||
# if self.current_track.playlist_tab == self.next_track.playlist_tab:
|
||||
# self.set_tab_colour(
|
||||
# self.current_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
|
||||
# )
|
||||
# else:
|
||||
# self.set_tab_colour(
|
||||
# self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
|
||||
# )
|
||||
if self.current_track.playlist_tab:
|
||||
if self.current_track.playlist_tab == self.next_track.playlist_tab:
|
||||
self.set_tab_colour(
|
||||
self.current_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
|
||||
)
|
||||
else:
|
||||
self.set_tab_colour(
|
||||
self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
|
||||
)
|
||||
|
||||
# Run end-of-track actions
|
||||
self.end_of_track_actions()
|
||||
@ -1544,11 +1671,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
with Session() as session:
|
||||
# Update self.next_track PlaylistTrack structure
|
||||
old_next_track = self.next_track
|
||||
self.next_track = NextTrack()
|
||||
self.next_track = PlaylistTrack()
|
||||
if next_plr_id:
|
||||
next_plr = session.get(PlaylistRows, next_plr_id)
|
||||
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
|
||||
# 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
|
||||
# new one or the current track, it needs its colour reset.
|
||||
return
|
||||
# TODO Reimplement
|
||||
# if (
|
||||
# old_next_track
|
||||
# and old_next_track.playlist_tab
|
||||
# and old_next_track.playlist_tab
|
||||
# not in [self.next_track.playlist_tab, self.current_track.playlist_tab]
|
||||
# ):
|
||||
# self.set_tab_colour(
|
||||
# old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
|
||||
# )
|
||||
# # If the new next playlist tab isn't the same as the
|
||||
# # old one or the current track, it needs its colour set.
|
||||
# if old_next_track:
|
||||
# old_tab = old_next_track.playlist_tab
|
||||
# else:
|
||||
# old_tab = None
|
||||
# if (
|
||||
# self.next_track
|
||||
# and self.next_track.playlist_tab
|
||||
# and self.next_track.playlist_tab
|
||||
# not in [old_tab, self.current_track.playlist_tab]
|
||||
# ):
|
||||
# self.set_tab_colour(
|
||||
# self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
|
||||
# )
|
||||
if (
|
||||
old_next_track
|
||||
and old_next_track.playlist_tab
|
||||
and old_next_track.playlist_tab
|
||||
not in [self.next_track.playlist_tab, self.current_track.playlist_tab]
|
||||
):
|
||||
self.set_tab_colour(
|
||||
old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
|
||||
)
|
||||
# If the new next playlist tab isn't the same as the
|
||||
# old one or the current track, it needs its colour set.
|
||||
if old_next_track:
|
||||
old_tab = old_next_track.playlist_tab
|
||||
else:
|
||||
old_tab = None
|
||||
if (
|
||||
self.next_track
|
||||
and self.next_track.playlist_tab
|
||||
and self.next_track.playlist_tab
|
||||
not in [old_tab, self.current_track.playlist_tab]
|
||||
):
|
||||
self.set_tab_colour(
|
||||
self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
|
||||
)
|
||||
|
||||
def tick_10ms(self) -> None:
|
||||
"""
|
||||
@ -1776,6 +1901,164 @@ class CartDialog(QDialog):
|
||||
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):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__()
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
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 (
|
||||
QAbstractTableModel,
|
||||
@ -14,11 +15,12 @@ from PyQt6.QtGui import (
|
||||
QFont,
|
||||
)
|
||||
|
||||
from classes import CurrentTrack, MusicMusterSignals, NextTrack
|
||||
from config import Config
|
||||
from datastructures import MusicMusterSignals
|
||||
from dbconfig import scoped_session, Session
|
||||
from helpers import file_is_unreadable
|
||||
from log import log
|
||||
from helpers import (
|
||||
file_is_unreadable,
|
||||
)
|
||||
from models import PlaylistRows, Tracks
|
||||
|
||||
|
||||
@ -102,7 +104,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.signals = MusicMusterSignals()
|
||||
|
||||
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:
|
||||
self.refresh_data(session)
|
||||
@ -137,49 +138,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# No track, no note, no point
|
||||
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:
|
||||
"""Return background setting"""
|
||||
|
||||
@ -363,13 +321,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
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:
|
||||
"""
|
||||
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
|
||||
|
||||
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 (
|
||||
QEvent,
|
||||
@ -22,7 +22,7 @@ from PyQt6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QHeaderView,
|
||||
QMenu,
|
||||
# QMenu,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QStyledItemDelegate,
|
||||
@ -35,9 +35,8 @@ from PyQt6.QtWidgets import (
|
||||
QStyleOption,
|
||||
)
|
||||
|
||||
from datastructures import MusicMusterSignals
|
||||
from dbconfig import Session, scoped_session
|
||||
from dialogs import TrackSelectDialog
|
||||
from classes import MusicMusterSignals
|
||||
from config import Config
|
||||
from helpers import (
|
||||
ask_yes_no,
|
||||
@ -49,11 +48,11 @@ from helpers import (
|
||||
set_track_metadata,
|
||||
)
|
||||
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:
|
||||
from musicmuster import Window
|
||||
from playlistmodel import PlaylistModel
|
||||
from musicmuster import Window, MusicMusterSignals
|
||||
|
||||
HEADER_NOTES_COLUMN = 2
|
||||
|
||||
@ -79,8 +78,7 @@ class EscapeDelegate(QStyledItemDelegate):
|
||||
Intercept createEditor call and make row just a little bit taller
|
||||
"""
|
||||
|
||||
self.signals = MusicMusterSignals()
|
||||
self.signals.enable_escape_signal.emit(False)
|
||||
signals.enable_escape_signal.emit(False)
|
||||
if isinstance(self.parent(), PlaylistTab):
|
||||
p = cast(PlaylistTab, self.parent())
|
||||
if isinstance(index.data(), str):
|
||||
@ -113,7 +111,7 @@ class EscapeDelegate(QStyledItemDelegate):
|
||||
return True
|
||||
elif key_event.key() == Qt.Key.Key_Escape:
|
||||
discard_edits = QMessageBox.question(
|
||||
cast(QWidget, self), "Abandon edit", "Discard changes?"
|
||||
self.parent(), "Abandon edit", "Discard changes?"
|
||||
)
|
||||
if discard_edits == QMessageBox.StandardButton.Yes:
|
||||
self.closeEditor.emit(editor)
|
||||
@ -136,7 +134,8 @@ class PlaylistStyle(QProxyStyle):
|
||||
def drawPrimitive(self, element, option, painter, widget=None):
|
||||
"""
|
||||
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 (
|
||||
element == QStyle.PrimitiveElement.PE_IndicatorItemViewItemDrop
|
||||
@ -179,9 +178,9 @@ class PlaylistTab(QTableView):
|
||||
# rows selected
|
||||
self.setDragEnabled(True)
|
||||
# Prepare for context menu
|
||||
self.menu = QMenu()
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._context_menu)
|
||||
# self.menu = QMenu()
|
||||
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
# self.customContextMenuRequested.connect(self._context_menu)
|
||||
|
||||
# Connect signals
|
||||
# This dancing is to satisfy mypy
|
||||
@ -292,27 +291,27 @@ class PlaylistTab(QTableView):
|
||||
|
||||
# self.hide_or_show_played_tracks()
|
||||
|
||||
def _add_context_menu(
|
||||
self,
|
||||
text: str,
|
||||
action: Callable,
|
||||
disabled: bool = False,
|
||||
parent_menu: Optional[QMenu] = None,
|
||||
) -> Optional[QAction]:
|
||||
"""
|
||||
Add item to self.menu
|
||||
"""
|
||||
# def _add_context_menu(
|
||||
# self,
|
||||
# text: str,
|
||||
# action: Callable,
|
||||
# disabled: bool = False,
|
||||
# parent_menu: Optional[QMenu] = None,
|
||||
# ) -> Optional[QAction]:
|
||||
# """
|
||||
# Add item to self.menu
|
||||
# """
|
||||
|
||||
if parent_menu is None:
|
||||
parent_menu = self.menu
|
||||
# if parent_menu is None:
|
||||
# parent_menu = self.menu
|
||||
|
||||
menu_item = parent_menu.addAction(text)
|
||||
if not menu_item:
|
||||
return None
|
||||
menu_item.setDisabled(disabled)
|
||||
menu_item.triggered.connect(action)
|
||||
# menu_item = parent_menu.addAction(text)
|
||||
# if not menu_item:
|
||||
# return None
|
||||
# menu_item.setDisabled(disabled)
|
||||
# menu_item.triggered.connect(action)
|
||||
|
||||
return menu_item
|
||||
# return menu_item
|
||||
|
||||
# def mouseReleaseEvent(self, event):
|
||||
# """
|
||||
@ -1034,26 +1033,42 @@ class PlaylistTab(QTableView):
|
||||
"""Add a track to a section header making it a normal track row"""
|
||||
|
||||
with Session() as session:
|
||||
dlg = TrackSelectDialog(
|
||||
session=session,
|
||||
new_row_number=row_number,
|
||||
playlist_id=self.playlist_id,
|
||||
add_to_header=True,
|
||||
)
|
||||
dlg.exec()
|
||||
# Add track to playlist row
|
||||
plr = self._get_row_plr(session, row_number)
|
||||
if not plr:
|
||||
return
|
||||
|
||||
def _build_context_menu(self, item: QTableWidgetItem) -> None:
|
||||
"""Used to process context (right-click) menu, which is defined here"""
|
||||
# Don't add track if there's already a track there
|
||||
if plr.track_id is not None:
|
||||
return
|
||||
|
||||
self.menu.clear()
|
||||
row_number = item.row()
|
||||
# Get track
|
||||
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_row = bool(track_id)
|
||||
header_row = False
|
||||
model = cast(PlaylistModel, self.model())
|
||||
if model:
|
||||
|
||||
header_row = model.is_header_row(row_number)
|
||||
# header_row = not track_row
|
||||
# current = row_number == self._get_current_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
|
||||
# if not current and not next_row:
|
||||
@ -1104,9 +1119,9 @@ class PlaylistTab(QTableView):
|
||||
# "Remove track from row", lambda: self._remove_track(row_number)
|
||||
# )
|
||||
|
||||
# Add track to section header (ie, make this a track row)
|
||||
if header_row:
|
||||
self._add_context_menu("Add a track", lambda: self._add_track(row_number))
|
||||
# # Add track to section header (ie, make this a track row)
|
||||
# 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):
|
||||
@ -1117,7 +1132,7 @@ class PlaylistTab(QTableView):
|
||||
# self._add_context_menu("Unmark as next track", self.clear_next)
|
||||
|
||||
# # ----------------------
|
||||
self.menu.addSeparator()
|
||||
# self.menu.addSeparator()
|
||||
|
||||
# # Sort
|
||||
# sort_menu = self.menu.addMenu("Sort")
|
||||
@ -1183,12 +1198,12 @@ class PlaylistTab(QTableView):
|
||||
record = Settings.get_int_settings(session, attr_name)
|
||||
record.f_int = self.columnWidth(column_number)
|
||||
|
||||
def _context_menu(self, pos):
|
||||
"""Display right-click menu"""
|
||||
# def _context_menu(self, pos):
|
||||
# """Display right-click menu"""
|
||||
|
||||
item = self.indexAt(pos)
|
||||
self._build_context_menu(item)
|
||||
self.menu.exec(self.mapToGlobal(pos))
|
||||
# item = self.itemAt(pos)
|
||||
# self._build_context_menu(item)
|
||||
# self.menu.exec(self.mapToGlobal(pos))
|
||||
|
||||
def _copy_path(self, row_number: int) -> None:
|
||||
"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user