Compare commits
No commits in common. "63340a408d8eda82341039a873e7259cf93dc174" and "0f1d5117cc14d619de6df2c6db2c539b82c1a038" have entirely different histories.
63340a408d
...
0f1d5117cc
@ -104,6 +104,10 @@ class PlaylistTrack:
|
||||
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
|
||||
|
||||
@ -32,6 +32,16 @@ class Config(object):
|
||||
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
||||
COLOUR_UNREADABLE = "#dc3545"
|
||||
COLOUR_WARNING_TIMER = "#ffc107"
|
||||
COLUMN_NAME_ARTIST = "Artist"
|
||||
COLUMN_NAME_AUTOPLAY = "A"
|
||||
COLUMN_NAME_BITRATE = "bps"
|
||||
COLUMN_NAME_END_TIME = "End"
|
||||
COLUMN_NAME_LAST_PLAYED = "Last played"
|
||||
COLUMN_NAME_LEADING_SILENCE = "Gap"
|
||||
COLUMN_NAME_LENGTH = "Length"
|
||||
COLUMN_NAME_NOTES = "Notes"
|
||||
COLUMN_NAME_START_TIME = "Start"
|
||||
COLUMN_NAME_TITLE = "Title"
|
||||
DBFS_SILENCE = -50
|
||||
DEBUG_FUNCTIONS: List[Optional[str]] = []
|
||||
DEBUG_MODULES: List[Optional[str]] = ["dbconfig"]
|
||||
|
||||
@ -31,7 +31,9 @@ def Session() -> Generator[scoped_session, None, None]:
|
||||
function = frame.function
|
||||
lineno = frame.lineno
|
||||
Session = scoped_session(sessionmaker(bind=engine))
|
||||
log.debug(f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]")
|
||||
log.debug(
|
||||
f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]"
|
||||
)
|
||||
yield Session
|
||||
log.debug(f" Session released [{hex(id(Session))}]")
|
||||
Session.commit()
|
||||
|
||||
@ -87,7 +87,7 @@ class TrackSelectDialog(QDialog):
|
||||
default_yes=True,
|
||||
):
|
||||
move_existing = True
|
||||
if self.add_to_header and existing_prd: # "and existing_prd" for mypy's benefit
|
||||
if self.add_to_header and existing_prd: # and existing_prd for mypy's benefit
|
||||
if move_existing:
|
||||
self.model.move_track_to_header(self.new_row_number, existing_prd, note)
|
||||
else:
|
||||
@ -95,7 +95,7 @@ class TrackSelectDialog(QDialog):
|
||||
# Close dialog - we can only add one track to a header
|
||||
self.accept()
|
||||
else:
|
||||
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
||||
if move_existing and existing_prd: # and existing_prd for mypy's benefit
|
||||
self.model.move_track_add_note(self.new_row_number, existing_prd, note)
|
||||
else:
|
||||
self.model.insert_row(self.new_row_number, track_id, note)
|
||||
|
||||
@ -14,7 +14,7 @@ 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
|
||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
|
||||
from tinytag import TinyTag # type: ignore
|
||||
|
||||
from config import Config
|
||||
@ -99,7 +99,7 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
||||
|
||||
|
||||
def get_embedded_time(text: str) -> Optional[datetime]:
|
||||
"""Return datetime specified as @hh:mm in text"""
|
||||
"""Return datetime specified as @hh:mm:ss in text"""
|
||||
|
||||
try:
|
||||
match = start_time_re.search(text)
|
||||
@ -171,7 +171,7 @@ def get_relative_date(
|
||||
weeks, days = divmod((reference_date.date() - past_date.date()).days, 7)
|
||||
if weeks == days == 0:
|
||||
# Same day so return time instead
|
||||
return Config.LAST_PLAYED_TODAY_STRING + " " + past_date.strftime("%H:%M")
|
||||
return past_date.strftime("%H:%M")
|
||||
if weeks == 1:
|
||||
weeks_str = "week"
|
||||
else:
|
||||
@ -226,6 +226,32 @@ def leading_silence(
|
||||
return min(trim_ms, len(audio_segment))
|
||||
|
||||
|
||||
def send_mail(to_addr, from_addr, subj, body):
|
||||
# From https://docs.python.org/3/library/email.examples.html
|
||||
|
||||
# Create a text/plain message
|
||||
msg = EmailMessage()
|
||||
msg.set_content(body)
|
||||
|
||||
msg["Subject"] = subj
|
||||
msg["From"] = from_addr
|
||||
msg["To"] = to_addr
|
||||
|
||||
# Send the message via SMTP server.
|
||||
context = ssl.create_default_context()
|
||||
try:
|
||||
s = smtplib.SMTP(host=Config.MAIL_SERVER, port=Config.MAIL_PORT)
|
||||
if Config.MAIL_USE_TLS:
|
||||
s.starttls(context=context)
|
||||
if Config.MAIL_USERNAME and Config.MAIL_PASSWORD:
|
||||
s.login(Config.MAIL_USERNAME, Config.MAIL_PASSWORD)
|
||||
s.send_message(msg)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
s.quit()
|
||||
|
||||
|
||||
def ms_to_mmss(
|
||||
ms: Optional[int],
|
||||
decimals: int = 0,
|
||||
@ -364,32 +390,6 @@ def open_in_audacity(path: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def send_mail(to_addr, from_addr, subj, body):
|
||||
# From https://docs.python.org/3/library/email.examples.html
|
||||
|
||||
# Create a text/plain message
|
||||
msg = EmailMessage()
|
||||
msg.set_content(body)
|
||||
|
||||
msg["Subject"] = subj
|
||||
msg["From"] = from_addr
|
||||
msg["To"] = to_addr
|
||||
|
||||
# Send the message via SMTP server.
|
||||
context = ssl.create_default_context()
|
||||
try:
|
||||
s = smtplib.SMTP(host=Config.MAIL_SERVER, port=Config.MAIL_PORT)
|
||||
if Config.MAIL_USE_TLS:
|
||||
s.starttls(context=context)
|
||||
if Config.MAIL_USERNAME and Config.MAIL_PASSWORD:
|
||||
s.login(Config.MAIL_USERNAME, Config.MAIL_PASSWORD)
|
||||
s.send_message(msg)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
s.quit()
|
||||
|
||||
|
||||
def set_track_metadata(track):
|
||||
"""Set/update track metadata in database"""
|
||||
|
||||
|
||||
@ -307,7 +307,8 @@ class Playlists(Base):
|
||||
"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls).where(cls.open.is_(True)).order_by(cls.tab)
|
||||
select(cls).where(cls.open.is_(True))
|
||||
.order_by(cls.tab)
|
||||
).all()
|
||||
|
||||
def mark_open(self) -> None:
|
||||
@ -322,10 +323,10 @@ class Playlists(Base):
|
||||
Return True if no playlist of this name exists else false.
|
||||
"""
|
||||
|
||||
return (
|
||||
session.execute(select(Playlists).where(Playlists.name == name)).first()
|
||||
is None
|
||||
)
|
||||
return session.execute(
|
||||
select(Playlists)
|
||||
.where(Playlists.name == name)
|
||||
).first() is None
|
||||
|
||||
def rename(self, session: scoped_session, new_name: str) -> None:
|
||||
"""
|
||||
@ -478,7 +479,9 @@ class PlaylistRows(Base):
|
||||
session.flush()
|
||||
|
||||
@staticmethod
|
||||
def delete_row(session: scoped_session, playlist_id: int, row_number: int) -> None:
|
||||
def delete_row(
|
||||
session: scoped_session, playlist_id: int, row_number: int
|
||||
) -> None:
|
||||
"""
|
||||
Delete passed row in given playlist.
|
||||
"""
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# import os
|
||||
import threading
|
||||
import vlc # type: ignore
|
||||
|
||||
#
|
||||
from config import Config
|
||||
from helpers import file_is_unreadable
|
||||
from typing import Optional
|
||||
@ -8,7 +10,7 @@ from time import sleep
|
||||
|
||||
from log import log
|
||||
|
||||
from PyQt6.QtCore import (
|
||||
from PyQt6.QtCore import ( # type: ignore
|
||||
QRunnable,
|
||||
QThreadPool,
|
||||
)
|
||||
|
||||
@ -65,7 +65,7 @@ from dbconfig import (
|
||||
from dialogs import TrackSelectDialog
|
||||
from log import log
|
||||
from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||
from playlistmodel import PlaylistModel
|
||||
from playlists import PlaylistTab
|
||||
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
|
||||
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
||||
@ -218,7 +218,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.active_tab = lambda: self.tabPlaylist.currentWidget()
|
||||
self.active_model = lambda: self.tabPlaylist.currentWidget().model()
|
||||
self.move_source_rows: Optional[List[int]] = None
|
||||
self.move_source_model: Optional[PlaylistProxyModel] = None
|
||||
self.move_source_model: Optional[PlaylistModel] = None
|
||||
|
||||
self.load_last_playlists()
|
||||
if Config.CARTS_HIDE:
|
||||
@ -1021,7 +1021,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
else:
|
||||
destination_row = self.active_model().rowCount()
|
||||
|
||||
if to_playlist_id == self.move_source_model.data_model.playlist_id:
|
||||
if to_playlist_id == self.move_source_model.playlist_id:
|
||||
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
||||
else:
|
||||
self.move_source_model.move_rows_between_playlists(
|
||||
|
||||
@ -13,7 +13,6 @@ from PyQt6.QtCore import (
|
||||
QRegularExpression,
|
||||
QSortFilterProxyModel,
|
||||
Qt,
|
||||
QTimer,
|
||||
QVariant,
|
||||
)
|
||||
from PyQt6.QtGui import (
|
||||
@ -171,7 +170,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if plr:
|
||||
# Add track to PlaylistRows
|
||||
plr.track_id = track_id
|
||||
# Add any further note (header will already have a note)
|
||||
# Add any further note
|
||||
if note:
|
||||
plr.note += "\n" + note
|
||||
# Reset header row spanning
|
||||
@ -263,11 +262,9 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Check for OBS scene change
|
||||
self.obs_scene_change(row_number)
|
||||
|
||||
# Update Playdates in database
|
||||
with Session() as session:
|
||||
# Update Playdates in database
|
||||
Playdates(session, track_sequence.now.track_id)
|
||||
|
||||
# Mark track as played in playlist
|
||||
plr = session.get(PlaylistRows, track_sequence.now.plr_id)
|
||||
if plr:
|
||||
plr.played = True
|
||||
@ -279,11 +276,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
# Update colour and times for current row
|
||||
self.invalidate_row(row_number)
|
||||
|
||||
# Update previous row in case we're hiding played rows
|
||||
if track_sequence.previous.plr_rownum:
|
||||
self.invalidate_row(track_sequence.previous.plr_rownum)
|
||||
|
||||
# Update all other track times
|
||||
self.update_track_times()
|
||||
|
||||
@ -313,29 +305,32 @@ class PlaylistModel(QAbstractTableModel):
|
||||
prd = self.playlist_rows[row]
|
||||
|
||||
# Dispatch to role-specific functions
|
||||
dispatch_table = {
|
||||
int(Qt.ItemDataRole.DisplayRole): self.display_role,
|
||||
int(Qt.ItemDataRole.EditRole): self.edit_role,
|
||||
int(Qt.ItemDataRole.FontRole): self.font_role,
|
||||
int(Qt.ItemDataRole.BackgroundRole): self.background_role,
|
||||
}
|
||||
|
||||
if role in dispatch_table:
|
||||
return dispatch_table[role](row, column, prd)
|
||||
|
||||
# Document other roles but don't use them
|
||||
if role in [
|
||||
Qt.ItemDataRole.DecorationRole,
|
||||
Qt.ItemDataRole.ToolTipRole,
|
||||
Qt.ItemDataRole.StatusTipRole,
|
||||
Qt.ItemDataRole.WhatsThisRole,
|
||||
Qt.ItemDataRole.SizeHintRole,
|
||||
Qt.ItemDataRole.TextAlignmentRole,
|
||||
Qt.ItemDataRole.ForegroundRole,
|
||||
Qt.ItemDataRole.CheckStateRole,
|
||||
Qt.ItemDataRole.InitialSortOrderRole,
|
||||
]:
|
||||
return QVariant()
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
return self.display_role(row, column, prd)
|
||||
elif role == Qt.ItemDataRole.DecorationRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.EditRole:
|
||||
return self.edit_role(row, column, prd)
|
||||
elif role == Qt.ItemDataRole.ToolTipRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.StatusTipRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.WhatsThisRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.SizeHintRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.FontRole:
|
||||
return self.font_role(row, column, prd)
|
||||
elif role == Qt.ItemDataRole.TextAlignmentRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
||||
return self.background_role(row, column, prd)
|
||||
elif role == Qt.ItemDataRole.ForegroundRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.CheckStateRole:
|
||||
pass
|
||||
elif role == Qt.ItemDataRole.InitialSortOrderRole:
|
||||
pass
|
||||
|
||||
# Fall through to no-op
|
||||
return QVariant()
|
||||
@ -356,51 +351,49 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
|
||||
self.refresh_data(session)
|
||||
self.reset_track_sequence_row_numbers()
|
||||
self.row_order_changed(self.playlist_id)
|
||||
|
||||
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
||||
"""
|
||||
Return text for display
|
||||
"""
|
||||
|
||||
if self.is_header_row(row):
|
||||
if not prd.path:
|
||||
# No track so this is a header row
|
||||
if column == HEADER_NOTES_COLUMN:
|
||||
self.signals.span_cells_signal.emit(
|
||||
row, HEADER_NOTES_COLUMN, 1, self.columnCount() - 1
|
||||
)
|
||||
header_text = self.header_text(prd)
|
||||
if not header_text:
|
||||
return QVariant(Config.TEXT_NO_TRACK_NO_NOTE)
|
||||
else:
|
||||
return QVariant(self.header_text(prd))
|
||||
return QVariant(self.header_text(prd))
|
||||
else:
|
||||
return QVariant()
|
||||
|
||||
if column == Col.START_GAP.value:
|
||||
return QVariant(prd.start_gap)
|
||||
if column == Col.TITLE.value:
|
||||
return QVariant(prd.title)
|
||||
if column == Col.ARTIST.value:
|
||||
return QVariant(prd.artist)
|
||||
if column == Col.DURATION.value:
|
||||
return QVariant(ms_to_mmss(prd.duration))
|
||||
if column == Col.START_TIME.value:
|
||||
if row in self.start_end_times:
|
||||
start_time = self.start_end_times[row].start_time
|
||||
if start_time:
|
||||
return QVariant(start_time.strftime(Config.TRACK_TIME_FORMAT))
|
||||
return QVariant()
|
||||
|
||||
if column == Col.END_TIME.value:
|
||||
if row in self.start_end_times:
|
||||
end_time = self.start_end_times[row].end_time
|
||||
if end_time:
|
||||
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
|
||||
return QVariant()
|
||||
|
||||
dispatch_table = {
|
||||
Col.START_GAP.value: QVariant(prd.start_gap),
|
||||
Col.TITLE.value: QVariant(prd.title),
|
||||
Col.ARTIST.value: QVariant(prd.artist),
|
||||
Col.DURATION.value: QVariant(ms_to_mmss(prd.duration)),
|
||||
Col.LAST_PLAYED.value: QVariant(get_relative_date(prd.lastplayed)),
|
||||
Col.BITRATE.value: QVariant(prd.bitrate),
|
||||
Col.NOTE.value: QVariant(prd.note),
|
||||
}
|
||||
if column in dispatch_table:
|
||||
return dispatch_table[column]
|
||||
if column == Col.LAST_PLAYED.value:
|
||||
return QVariant(get_relative_date(prd.lastplayed))
|
||||
if column == Col.BITRATE.value:
|
||||
return QVariant(prd.bitrate)
|
||||
if column == Col.NOTE.value:
|
||||
return QVariant(prd.note)
|
||||
|
||||
return QVariant()
|
||||
|
||||
@ -414,7 +407,27 @@ class PlaylistModel(QAbstractTableModel):
|
||||
with Session() as session:
|
||||
self.refresh_data(session)
|
||||
super().endResetModel()
|
||||
self.reset_track_sequence_row_numbers()
|
||||
self.row_order_changed(self.playlist_id)
|
||||
|
||||
def get_duplicate_rows(self) -> List[int]:
|
||||
"""
|
||||
Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4]
|
||||
(ie, ignore the first, not-yet-duplicate, track).
|
||||
"""
|
||||
|
||||
found = []
|
||||
result = []
|
||||
|
||||
for i in range(len(self.playlist_rows)):
|
||||
track_id = self.playlist_rows[i].track_id
|
||||
if track_id is None:
|
||||
continue
|
||||
if track_id in found:
|
||||
result.append(i)
|
||||
else:
|
||||
found.append(track_id)
|
||||
|
||||
return result
|
||||
|
||||
def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
||||
"""
|
||||
@ -467,26 +480,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return QVariant(boldfont)
|
||||
|
||||
def get_duplicate_rows(self) -> List[int]:
|
||||
"""
|
||||
Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4]
|
||||
(ie, ignore the first, not-yet-duplicate, track).
|
||||
"""
|
||||
|
||||
found = []
|
||||
result = []
|
||||
|
||||
for i in range(len(self.playlist_rows)):
|
||||
track_id = self.playlist_rows[i].track_id
|
||||
if track_id is None:
|
||||
continue
|
||||
if track_id in found:
|
||||
result.append(i)
|
||||
else:
|
||||
found.append(track_id)
|
||||
|
||||
return result
|
||||
|
||||
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
|
||||
"""
|
||||
Sanitises proposed new row number.
|
||||
@ -506,13 +499,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return new_row_number
|
||||
|
||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||
"""
|
||||
Return info about passed row
|
||||
"""
|
||||
|
||||
return self.playlist_rows[row_number]
|
||||
|
||||
def get_row_track_path(self, row_number: int) -> str:
|
||||
"""
|
||||
Return path of track associated with row or empty string if no track associated
|
||||
@ -531,6 +517,13 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return duration
|
||||
|
||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||
"""
|
||||
Return info about passed row
|
||||
"""
|
||||
|
||||
return self.playlist_rows[row_number]
|
||||
|
||||
def get_unplayed_rows(self) -> List[int]:
|
||||
"""
|
||||
Return a list of unplayed row numbers
|
||||
@ -660,19 +653,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
unplayed_count += 1
|
||||
duration += row_prd.duration
|
||||
|
||||
elif prd.note == "-":
|
||||
# If the hyphen is the only thing on the line, echo the note
|
||||
# tha started the section without the trailing "+".
|
||||
for row_number in range(prd.plr_rownum - 1, -1, -1):
|
||||
row_prd = self.playlist_rows[row_number]
|
||||
if self.is_header_row(row_number):
|
||||
if row_prd.note.endswith("-"):
|
||||
# We didn't find a matching section start
|
||||
break
|
||||
if row_prd.note.endswith("+"):
|
||||
return f"[End: {row_prd.note[:-1]}]"
|
||||
return "-"
|
||||
|
||||
return prd.note
|
||||
|
||||
def hide_played_tracks(self, hide: bool) -> None:
|
||||
@ -685,6 +665,22 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if self.is_played_row(row_number):
|
||||
self.invalidate_row(row_number)
|
||||
|
||||
def is_header_row(self, row_number: int) -> bool:
|
||||
"""
|
||||
Return True if row is a header row, else False
|
||||
"""
|
||||
|
||||
if row_number in self.playlist_rows:
|
||||
return self.playlist_rows[row_number].path == ""
|
||||
return False
|
||||
|
||||
def is_played_row(self, row_number: int) -> bool:
|
||||
"""
|
||||
Return True if row is an unplayed track row, else False
|
||||
"""
|
||||
|
||||
return self.playlist_rows[row_number].played
|
||||
|
||||
def insert_row(
|
||||
self,
|
||||
proposed_row_number: Optional[int],
|
||||
@ -708,7 +704,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.refresh_data(session)
|
||||
super().endInsertRows()
|
||||
|
||||
self.reset_track_sequence_row_numbers()
|
||||
self.row_order_changed(self.playlist_id)
|
||||
self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))))
|
||||
|
||||
def invalidate_row(self, modified_row: int) -> None:
|
||||
@ -729,22 +725,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
for modified_row in modified_rows:
|
||||
self.invalidate_row(modified_row)
|
||||
|
||||
def is_header_row(self, row_number: int) -> bool:
|
||||
"""
|
||||
Return True if row is a header row, else False
|
||||
"""
|
||||
|
||||
if row_number in self.playlist_rows:
|
||||
return self.playlist_rows[row_number].path == ""
|
||||
return False
|
||||
|
||||
def is_played_row(self, row_number: int) -> bool:
|
||||
"""
|
||||
Return True if row is an unplayed track row, else False
|
||||
"""
|
||||
|
||||
return self.playlist_rows[row_number].played
|
||||
|
||||
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
||||
"""
|
||||
If this track_id is in the playlist, return the PlaylistRowData object
|
||||
@ -757,6 +737,21 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return None
|
||||
|
||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||
"""
|
||||
Mark row as unplayed
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
for row_number in row_numbers:
|
||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||
if not plr:
|
||||
return
|
||||
plr.played = False
|
||||
self.refresh_row(session, row_number)
|
||||
|
||||
self.invalidate_rows(row_numbers)
|
||||
|
||||
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
||||
"""
|
||||
Move the playlist rows given to to_row and below.
|
||||
@ -790,15 +785,12 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Optimise: only add to map if there is a change
|
||||
if old_row != new_row:
|
||||
row_map[old_row] = new_row
|
||||
if self.is_header_row(old_row):
|
||||
# Reset column span
|
||||
self.signals.span_cells_signal.emit(
|
||||
old_row, HEADER_NOTES_COLUMN, 1, 1
|
||||
)
|
||||
|
||||
# Reset any header rows that we're moving
|
||||
for moving_row in row_map:
|
||||
if self.is_header_row(moving_row):
|
||||
# Reset column span
|
||||
print(f"Reset column span {moving_row=}")
|
||||
self.signals.span_cells_signal.emit(
|
||||
moving_row, HEADER_NOTES_COLUMN, 1, 1
|
||||
)
|
||||
# Check to see whether any rows in track_sequence have moved
|
||||
if track_sequence.previous.plr_rownum in row_map:
|
||||
track_sequence.previous.plr_rownum = row_map[
|
||||
@ -822,24 +814,9 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.refresh_data(session)
|
||||
|
||||
# Update display
|
||||
self.reset_track_sequence_row_numbers()
|
||||
self.signals.row_order_changed_signal.emit(self.playlist_id)
|
||||
self.invalidate_rows(list(row_map.keys()))
|
||||
|
||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||
"""
|
||||
Mark row as unplayed
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
for row_number in row_numbers:
|
||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||
if not plr:
|
||||
return
|
||||
plr.played = False
|
||||
self.refresh_row(session, row_number)
|
||||
|
||||
self.invalidate_rows(row_numbers)
|
||||
|
||||
def move_rows_between_playlists(
|
||||
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
||||
) -> None:
|
||||
@ -890,7 +867,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.refresh_data(session)
|
||||
|
||||
# Reset of model must come after session has been closed
|
||||
self.reset_track_sequence_row_numbers()
|
||||
self.signals.row_order_changed_signal.emit(self.playlist_id)
|
||||
self.signals.row_order_changed_signal.emit(to_playlist_id)
|
||||
self.signals.end_reset_model_signal.emit(to_playlist_id)
|
||||
self.update_track_times()
|
||||
@ -980,6 +957,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Actions required:
|
||||
- sanity check
|
||||
- update display
|
||||
- update track times
|
||||
"""
|
||||
|
||||
# Sanity check
|
||||
@ -1036,31 +1014,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.invalidate_row(row_number)
|
||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
||||
|
||||
def reset_track_sequence_row_numbers(self) -> None:
|
||||
"""
|
||||
Signal handler for when row ordering has changed
|
||||
"""
|
||||
|
||||
# Check the track_sequence next, now and previous plrs and
|
||||
# update the row number
|
||||
with Session() as session:
|
||||
if track_sequence.next.plr_rownum:
|
||||
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
|
||||
if next_plr:
|
||||
track_sequence.next.plr_rownum = next_plr.plr_rownum
|
||||
if track_sequence.now.plr_rownum:
|
||||
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id)
|
||||
if now_plr:
|
||||
track_sequence.now.plr_rownum = now_plr.plr_rownum
|
||||
if track_sequence.previous.plr_rownum:
|
||||
previous_plr = session.get(
|
||||
PlaylistRows, track_sequence.previous.plr_id
|
||||
)
|
||||
if previous_plr:
|
||||
track_sequence.previous.plr_rownum = previous_plr.plr_rownum
|
||||
|
||||
self.update_track_times()
|
||||
|
||||
def _reversed_contiguous_row_groups(
|
||||
self, row_numbers: List[int]
|
||||
) -> List[List[int]]:
|
||||
@ -1099,11 +1052,26 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Signal handler for when row ordering has changed
|
||||
"""
|
||||
|
||||
# Only action if this is for us
|
||||
if playlist_id != self.playlist_id:
|
||||
return
|
||||
|
||||
self.reset_track_sequence_row_numbers()
|
||||
with Session() as session:
|
||||
if track_sequence.next.plr_rownum:
|
||||
next_plr = session.get(PlaylistRows, track_sequence.next.plr_rownum)
|
||||
if next_plr:
|
||||
track_sequence.next.plr_rownum = next_plr.plr_rownum
|
||||
if track_sequence.now.plr_rownum:
|
||||
now_plr = session.get(PlaylistRows, track_sequence.now.plr_rownum)
|
||||
if now_plr:
|
||||
track_sequence.now.plr_rownum = now_plr.plr_rownum
|
||||
if track_sequence.previous.plr_rownum:
|
||||
previous_plr = session.get(
|
||||
PlaylistRows, track_sequence.previous.plr_rownum
|
||||
)
|
||||
if previous_plr:
|
||||
track_sequence.previous.plr_rownum = previous_plr.plr_rownum
|
||||
|
||||
self.update_track_times()
|
||||
|
||||
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
|
||||
"""
|
||||
@ -1134,6 +1102,8 @@ class PlaylistModel(QAbstractTableModel):
|
||||
"""
|
||||
|
||||
next_row_was = track_sequence.next.plr_rownum
|
||||
if next_row_was is not None:
|
||||
self.invalidate_row(next_row_was)
|
||||
|
||||
if row_number is None:
|
||||
if next_row_was is None:
|
||||
@ -1142,7 +1112,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.signals.next_track_changed_signal.emit()
|
||||
return
|
||||
|
||||
# Update playing_track
|
||||
# Update playing_trtack
|
||||
with Session() as session:
|
||||
track_sequence.next = PlaylistTrack()
|
||||
try:
|
||||
@ -1167,9 +1137,6 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.playlist_rows[row_number].title
|
||||
)
|
||||
self.invalidate_row(row_number)
|
||||
|
||||
if next_row_was is not None:
|
||||
self.invalidate_row(next_row_was)
|
||||
self.update_track_times()
|
||||
|
||||
def setData(
|
||||
@ -1301,6 +1268,10 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if prd.played:
|
||||
continue
|
||||
|
||||
# Don't schedule unplayable tracks
|
||||
if file_is_unreadable(prd.path):
|
||||
continue
|
||||
|
||||
# If we're between the current and next row, zero out
|
||||
# times
|
||||
if (
|
||||
@ -1319,12 +1290,8 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if header_time:
|
||||
next_start_time = header_time
|
||||
else:
|
||||
# This is an unplayed track
|
||||
# Don't schedule unplayable tracks
|
||||
if file_is_unreadable(prd.path):
|
||||
continue
|
||||
|
||||
# Set start/end if we have a start time
|
||||
# This is an unplayed track; set start/end if we have a
|
||||
# start time
|
||||
if next_start_time is None:
|
||||
continue
|
||||
if stend.start_time != next_start_time:
|
||||
@ -1352,14 +1319,14 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_model: PlaylistModel,
|
||||
playlist_model: PlaylistModel,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.data_model = data_model
|
||||
self.playlist_model = playlist_model
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setSourceModel(data_model)
|
||||
self.setSourceModel(playlist_model)
|
||||
# Search all columns
|
||||
self.setFilterKeyColumn(-1)
|
||||
|
||||
@ -1368,8 +1335,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
Subclass to filter by played status
|
||||
"""
|
||||
|
||||
if self.data_model.played_tracks_hidden:
|
||||
if self.data_model.is_played_row(source_row):
|
||||
if self.playlist_model.played_tracks_hidden:
|
||||
if self.playlist_model.is_played_row(source_row):
|
||||
# Don't hide current or next track
|
||||
with Session() as session:
|
||||
if track_sequence.next.plr_id:
|
||||
@ -1377,7 +1344,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
if (
|
||||
next_plr
|
||||
and next_plr.plr_rownum == source_row
|
||||
and next_plr.playlist_id == self.data_model.playlist_id
|
||||
and next_plr.playlist_id == self.playlist_model.playlist_id
|
||||
):
|
||||
return True
|
||||
if track_sequence.now.plr_id:
|
||||
@ -1385,44 +1352,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
if (
|
||||
now_plr
|
||||
and now_plr.plr_rownum == source_row
|
||||
and now_plr.playlist_id == self.data_model.playlist_id
|
||||
and now_plr.playlist_id == self.playlist_model.playlist_id
|
||||
):
|
||||
return True
|
||||
# Don't hide previous track until
|
||||
# HIDE_AFTER_PLAYING_OFFSET milliseconds after
|
||||
# current track has started
|
||||
if track_sequence.previous.plr_id:
|
||||
previous_plr = session.get(
|
||||
PlaylistRows, track_sequence.previous.plr_id
|
||||
)
|
||||
if (
|
||||
previous_plr
|
||||
and previous_plr.plr_rownum == source_row
|
||||
and previous_plr.playlist_id == self.data_model.playlist_id
|
||||
):
|
||||
if track_sequence.now.start_time:
|
||||
if datetime.now() > (
|
||||
track_sequence.now.start_time
|
||||
+ timedelta(
|
||||
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
|
||||
)
|
||||
):
|
||||
return False
|
||||
else:
|
||||
# Invalidate this row in
|
||||
# HIDE_AFTER_PLAYING_OFFSET and a
|
||||
# bit milliseconds
|
||||
# so that it hides then - add 100mS
|
||||
# on so that it if clause above it
|
||||
# true next time through.
|
||||
QTimer.singleShot(
|
||||
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
||||
lambda: self.data_model.invalidate_row(source_row),
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
return False
|
||||
return super().filterAcceptsRow(source_row, source_parent)
|
||||
|
||||
@ -1442,25 +1374,25 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
# ######################################
|
||||
|
||||
def current_track_started(self):
|
||||
return self.data_model.current_track_started()
|
||||
return self.playlist_model.current_track_started()
|
||||
|
||||
def delete_rows(self, row_numbers: List[int]) -> None:
|
||||
return self.data_model.delete_rows(row_numbers)
|
||||
return self.playlist_model.delete_rows(row_numbers)
|
||||
|
||||
def get_duplicate_rows(self) -> List[int]:
|
||||
return self.data_model.get_duplicate_rows()
|
||||
return self.playlist_model.get_duplicate_rows()
|
||||
|
||||
def get_rows_duration(self, row_numbers: List[int]) -> int:
|
||||
return self.data_model.get_rows_duration(row_numbers)
|
||||
return self.playlist_model.get_rows_duration(row_numbers)
|
||||
|
||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||
return self.data_model.get_row_info(row_number)
|
||||
return self.playlist_model.get_row_info(row_number)
|
||||
|
||||
def get_row_track_path(self, row_number: int) -> str:
|
||||
return self.data_model.get_row_track_path(row_number)
|
||||
return self.playlist_model.get_row_track_path(row_number)
|
||||
|
||||
def hide_played_tracks(self, hide: bool) -> None:
|
||||
return self.data_model.hide_played_tracks(hide)
|
||||
return self.playlist_model.hide_played_tracks(hide)
|
||||
|
||||
def insert_row(
|
||||
self,
|
||||
@ -1468,68 +1400,70 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
track_id: Optional[int] = None,
|
||||
note: Optional[str] = None,
|
||||
) -> None:
|
||||
return self.data_model.insert_row(proposed_row_number, track_id, note)
|
||||
return self.playlist_model.insert_row(proposed_row_number, track_id, note)
|
||||
|
||||
def is_header_row(self, row_number: int) -> bool:
|
||||
return self.data_model.is_header_row(row_number)
|
||||
return self.playlist_model.is_header_row(row_number)
|
||||
|
||||
def is_played_row(self, row_number: int) -> bool:
|
||||
return self.data_model.is_played_row(row_number)
|
||||
return self.playlist_model.is_played_row(row_number)
|
||||
|
||||
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
||||
return self.data_model.is_track_in_playlist(track_id)
|
||||
return self.playlist_model.is_track_in_playlist(track_id)
|
||||
|
||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||
return self.data_model.mark_unplayed(row_numbers)
|
||||
return self.playlist_model.mark_unplayed(row_numbers)
|
||||
|
||||
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
||||
return self.data_model.move_rows(from_rows, to_row_number)
|
||||
return self.playlist_model.move_rows(from_rows, to_row_number)
|
||||
|
||||
def move_rows_between_playlists(
|
||||
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
||||
) -> None:
|
||||
return self.data_model.move_rows_between_playlists(
|
||||
return self.playlist_model.move_rows_between_playlists(
|
||||
from_rows, to_row_number, to_playlist_id
|
||||
)
|
||||
|
||||
def move_track_add_note(
|
||||
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
|
||||
) -> None:
|
||||
return self.data_model.move_track_add_note(new_row_number, existing_prd, note)
|
||||
return self.playlist_model.move_track_add_note(
|
||||
new_row_number, existing_prd, note
|
||||
)
|
||||
|
||||
def move_track_to_header(
|
||||
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
|
||||
) -> None:
|
||||
return self.data_model.move_track_to_header(
|
||||
return self.playlist_model.move_track_to_header(
|
||||
header_row_number, existing_prd, note
|
||||
)
|
||||
|
||||
def open_in_audacity(self, row_number: int) -> None:
|
||||
return self.data_model.open_in_audacity(row_number)
|
||||
return self.playlist_model.open_in_audacity(row_number)
|
||||
|
||||
def previous_track_ended(self) -> None:
|
||||
return self.data_model.previous_track_ended()
|
||||
return self.playlist_model.previous_track_ended()
|
||||
|
||||
def remove_track(self, row_number: int) -> None:
|
||||
return self.data_model.remove_track(row_number)
|
||||
return self.playlist_model.remove_track(row_number)
|
||||
|
||||
def rescan_track(self, row_number: int) -> None:
|
||||
return self.data_model.rescan_track(row_number)
|
||||
return self.playlist_model.rescan_track(row_number)
|
||||
|
||||
def set_next_row(self, row_number: Optional[int]) -> None:
|
||||
return self.data_model.set_next_row(row_number)
|
||||
return self.playlist_model.set_next_row(row_number)
|
||||
|
||||
def sort_by_artist(self, row_numbers: List[int]) -> None:
|
||||
return self.data_model.sort_by_artist(row_numbers)
|
||||
return self.playlist_model.sort_by_artist(row_numbers)
|
||||
|
||||
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
||||
return self.data_model.sort_by_duration(row_numbers)
|
||||
return self.playlist_model.sort_by_duration(row_numbers)
|
||||
|
||||
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
||||
return self.data_model.sort_by_lastplayed(row_numbers)
|
||||
return self.playlist_model.sort_by_lastplayed(row_numbers)
|
||||
|
||||
def sort_by_title(self, row_numbers: List[int]) -> None:
|
||||
return self.data_model.sort_by_title(row_numbers)
|
||||
return self.playlist_model.sort_by_title(row_numbers)
|
||||
|
||||
def update_track_times(self) -> None:
|
||||
return self.data_model.update_track_times()
|
||||
return self.playlist_model.update_track_times()
|
||||
|
||||
299
app/playlists.py
299
app/playlists.py
@ -34,7 +34,6 @@ from config import Config
|
||||
from helpers import (
|
||||
ask_yes_no,
|
||||
ms_to_mmss,
|
||||
show_OK,
|
||||
show_warning,
|
||||
)
|
||||
from models import Settings
|
||||
@ -51,9 +50,9 @@ class EscapeDelegate(QStyledItemDelegate):
|
||||
- checks with user before abandoning edit on Escape
|
||||
"""
|
||||
|
||||
def __init__(self, parent, data_model: PlaylistModel) -> None:
|
||||
def __init__(self, parent, playlist_model: PlaylistModel) -> None:
|
||||
super().__init__(parent)
|
||||
self.data_model = data_model
|
||||
self.playlist_model = playlist_model
|
||||
self.signals = MusicMusterSignals()
|
||||
|
||||
def createEditor(
|
||||
@ -114,7 +113,7 @@ class EscapeDelegate(QStyledItemDelegate):
|
||||
else:
|
||||
edit_index = index
|
||||
|
||||
value = self.data_model.data(edit_index, Qt.ItemDataRole.EditRole)
|
||||
value = self.playlist_model.data(edit_index, Qt.ItemDataRole.EditRole)
|
||||
editor.setPlainText(value.value())
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
@ -125,7 +124,7 @@ class EscapeDelegate(QStyledItemDelegate):
|
||||
edit_index = index
|
||||
|
||||
value = editor.toPlainText().strip()
|
||||
self.data_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
||||
self.playlist_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
editor.setGeometry(option.rect)
|
||||
@ -150,10 +149,6 @@ class PlaylistStyle(QProxyStyle):
|
||||
|
||||
|
||||
class PlaylistTab(QTableView):
|
||||
"""
|
||||
The playlist view
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
musicmuster: "Window",
|
||||
@ -166,9 +161,9 @@ class PlaylistTab(QTableView):
|
||||
self.playlist_id = playlist_id
|
||||
|
||||
# Set up widget
|
||||
self.data_model = PlaylistModel(playlist_id)
|
||||
self.proxy_model = PlaylistProxyModel(self.data_model)
|
||||
self.setItemDelegate(EscapeDelegate(self, self.data_model))
|
||||
self.playlist_model = PlaylistModel(playlist_id)
|
||||
self.proxy_model = PlaylistProxyModel(self.playlist_model)
|
||||
self.setItemDelegate(EscapeDelegate(self, self.playlist_model))
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||
@ -195,6 +190,10 @@ class PlaylistTab(QTableView):
|
||||
self.signals.resize_rows_signal.connect(self.resizeRowsToContents)
|
||||
self.signals.span_cells_signal.connect(self._span_cells)
|
||||
|
||||
# Initialise miscellaneous instance variables
|
||||
self.search_text: str = ""
|
||||
self.sort_undo: List[int] = []
|
||||
|
||||
# Selection model
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
@ -203,8 +202,6 @@ class PlaylistTab(QTableView):
|
||||
self.setModel(self.proxy_model)
|
||||
self._set_column_widths()
|
||||
|
||||
# ########## Overrident class functions ##########
|
||||
|
||||
def closeEditor(
|
||||
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
|
||||
) -> None:
|
||||
@ -223,7 +220,7 @@ class PlaylistTab(QTableView):
|
||||
|
||||
# Update start times in case a start time in a note has been
|
||||
# edited
|
||||
self.data_model.update_track_times()
|
||||
self.playlist_model.update_track_times()
|
||||
|
||||
def dropEvent(self, event):
|
||||
if event.source() is not self or (
|
||||
@ -243,7 +240,6 @@ class PlaylistTab(QTableView):
|
||||
|
||||
# Reset drag mode to allow row selection by dragging
|
||||
self.setDragEnabled(False)
|
||||
|
||||
# Deselect rows
|
||||
self.clear_selection()
|
||||
|
||||
@ -256,7 +252,7 @@ class PlaylistTab(QTableView):
|
||||
event: Optional[QEvent],
|
||||
) -> bool:
|
||||
"""
|
||||
Override QAbstractItemView.edit to catch when editing starts
|
||||
Override PySide2.QAbstractItemView.edit to catch when editing starts
|
||||
|
||||
Editing only ever starts with a double click on a cell
|
||||
"""
|
||||
@ -268,43 +264,6 @@ class PlaylistTab(QTableView):
|
||||
|
||||
return result
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Enable dragging if rows are selected
|
||||
"""
|
||||
|
||||
if self.selectedIndexes():
|
||||
self.setDragEnabled(True)
|
||||
else:
|
||||
self.setDragEnabled(False)
|
||||
self.reset()
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def selectionChanged(
|
||||
self, selected: QItemSelection, deselected: QItemSelection
|
||||
) -> None:
|
||||
"""
|
||||
Toggle drag behaviour according to whether rows are selected
|
||||
"""
|
||||
|
||||
selected_rows = self.get_selected_rows()
|
||||
# If no rows are selected, we have nothing to do
|
||||
if len(selected_rows) == 0:
|
||||
self.musicmuster.lblSumPlaytime.setText("")
|
||||
else:
|
||||
selected_duration = self.data_model.get_rows_duration(
|
||||
self.get_selected_rows()
|
||||
)
|
||||
if selected_duration > 0:
|
||||
self.musicmuster.lblSumPlaytime.setText(
|
||||
f"Selected duration: {ms_to_mmss(selected_duration)}"
|
||||
)
|
||||
else:
|
||||
self.musicmuster.lblSumPlaytime.setText("")
|
||||
|
||||
super().selectionChanged(selected, deselected)
|
||||
|
||||
# ########## Custom functions ##########
|
||||
def _add_context_menu(
|
||||
self,
|
||||
text: str,
|
||||
@ -327,6 +286,121 @@ class PlaylistTab(QTableView):
|
||||
|
||||
return menu_item
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""
|
||||
Enable dragging if rows are selected
|
||||
"""
|
||||
|
||||
if self.selectedIndexes():
|
||||
self.setDragEnabled(True)
|
||||
else:
|
||||
self.setDragEnabled(False)
|
||||
self.reset()
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
# # ########## Externally called functions ##########
|
||||
|
||||
def clear_selection(self) -> None:
|
||||
"""Unselect all tracks and reset drag mode"""
|
||||
|
||||
self.clearSelection()
|
||||
self.setDragEnabled(False)
|
||||
|
||||
def selected_display_row_number(self):
|
||||
"""
|
||||
Return the selected row number or None if none selected.
|
||||
"""
|
||||
|
||||
row_index = self._selected_row_index()
|
||||
if row_index:
|
||||
return row_index.row()
|
||||
else:
|
||||
return None
|
||||
return row_index.row()
|
||||
|
||||
def selected_display_row_numbers(self):
|
||||
"""
|
||||
Return a list of the selected row numbers
|
||||
"""
|
||||
|
||||
indexes = self._selected_row_indexes()
|
||||
|
||||
return [a.row() for a in indexes]
|
||||
|
||||
def selected_model_row_number(self) -> Optional[int]:
|
||||
"""
|
||||
Return the model row number corresponding to the selected row or None
|
||||
"""
|
||||
|
||||
selected_index = self._selected_row_index()
|
||||
if selected_index is None:
|
||||
return None
|
||||
if hasattr(self.proxy_model, "mapToSource"):
|
||||
return self.proxy_model.mapToSource(selected_index).row()
|
||||
return selected_index.row()
|
||||
|
||||
def selected_model_row_numbers(self) -> List[int]:
|
||||
"""
|
||||
Return a list of model row numbers corresponding to the selected rows or
|
||||
an empty list.
|
||||
"""
|
||||
|
||||
selected_indexes = self._selected_row_indexes()
|
||||
if selected_indexes is None:
|
||||
return []
|
||||
if hasattr(self.proxy_model, "mapToSource"):
|
||||
return [self.proxy_model.mapToSource(a).row() for a in selected_indexes]
|
||||
return [a.row() for a in selected_indexes]
|
||||
|
||||
def _selected_row_index(self) -> Optional[QModelIndex]:
|
||||
"""
|
||||
Return the selected row index or None if none selected.
|
||||
"""
|
||||
|
||||
row_indexes = self._selected_row_indexes()
|
||||
|
||||
if len(row_indexes) != 1:
|
||||
show_warning(
|
||||
self.musicmuster, "No or multiple rows selected", "Select only one row"
|
||||
)
|
||||
return None
|
||||
|
||||
return row_indexes[0]
|
||||
|
||||
def _selected_row_indexes(self) -> List[QModelIndex]:
|
||||
"""
|
||||
Return a list of indexes of column 1 of selected rows
|
||||
"""
|
||||
|
||||
sm = self.selectionModel()
|
||||
if sm and sm.hasSelection():
|
||||
return sm.selectedRows()
|
||||
return []
|
||||
|
||||
def get_selected_row_track_path(self) -> str:
|
||||
"""
|
||||
Return the path of the selected row. If no row selected or selected
|
||||
row does not have a track, return empty string.
|
||||
"""
|
||||
|
||||
model_row_number = self.selected_model_row_number()
|
||||
if model_row_number is None:
|
||||
return ""
|
||||
return self.playlist_model.get_row_track_path(model_row_number)
|
||||
|
||||
def set_row_as_next_track(self) -> None:
|
||||
"""
|
||||
Set selected row as next track
|
||||
"""
|
||||
|
||||
model_row_number = self.selected_model_row_number()
|
||||
if model_row_number is None:
|
||||
return
|
||||
self.playlist_model.set_next_row(model_row_number)
|
||||
self.clearSelection()
|
||||
|
||||
# # # ########## Internally called functions ##########
|
||||
|
||||
def _add_track(self) -> None:
|
||||
"""Add a track to a section header making it a normal track row"""
|
||||
|
||||
@ -338,7 +412,7 @@ class PlaylistTab(QTableView):
|
||||
dlg = TrackSelectDialog(
|
||||
session=session,
|
||||
new_row_number=model_row_number,
|
||||
model=self.data_model,
|
||||
model=self.playlist_model,
|
||||
add_to_header=True,
|
||||
)
|
||||
dlg.exec()
|
||||
@ -442,12 +516,6 @@ class PlaylistTab(QTableView):
|
||||
"Copy track path", lambda: self._copy_path(model_row_number)
|
||||
)
|
||||
|
||||
def clear_selection(self) -> None:
|
||||
"""Unselect all tracks and reset drag mode"""
|
||||
|
||||
self.clearSelection()
|
||||
self.setDragEnabled(False)
|
||||
|
||||
def _column_resize(self, column_number: int, _old: int, _new: int) -> None:
|
||||
"""
|
||||
Called when column width changes. Save new width to database.
|
||||
@ -478,7 +546,7 @@ class PlaylistTab(QTableView):
|
||||
to the clipboard. Otherwise, return None.
|
||||
"""
|
||||
|
||||
track_path = self.data_model.get_row_info(row_number).path
|
||||
track_path = self.playlist_model.get_row_info(row_number).path
|
||||
if not track_path:
|
||||
return
|
||||
|
||||
@ -515,20 +583,9 @@ class PlaylistTab(QTableView):
|
||||
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
||||
return
|
||||
|
||||
self.data_model.delete_rows(self.selected_model_row_numbers())
|
||||
self.playlist_model.delete_rows(self.selected_model_row_numbers())
|
||||
self.clear_selection()
|
||||
|
||||
def get_selected_row_track_path(self) -> str:
|
||||
"""
|
||||
Return the path of the selected row. If no row selected or selected
|
||||
row does not have a track, return empty string.
|
||||
"""
|
||||
|
||||
model_row_number = self.selected_model_row_number()
|
||||
if model_row_number is None:
|
||||
return ""
|
||||
return self.data_model.get_row_track_path(model_row_number)
|
||||
|
||||
def get_selected_rows(self) -> List[int]:
|
||||
"""Return a list of selected row numbers sorted by row"""
|
||||
|
||||
@ -539,7 +596,7 @@ class PlaylistTab(QTableView):
|
||||
def _info_row(self, row_number: int) -> None:
|
||||
"""Display popup with info re row"""
|
||||
|
||||
prd = self.data_model.get_row_info(row_number)
|
||||
prd = self.playlist_model.get_row_info(row_number)
|
||||
if prd:
|
||||
txt = (
|
||||
f"Title: {prd.title}\n"
|
||||
@ -553,18 +610,23 @@ class PlaylistTab(QTableView):
|
||||
else:
|
||||
txt = f"Can't find info about row{row_number}"
|
||||
|
||||
show_OK(self.musicmuster, "Track info", txt)
|
||||
info: QMessageBox = QMessageBox(self)
|
||||
info.setIcon(QMessageBox.Icon.Information)
|
||||
info.setText(txt)
|
||||
info.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
info.setDefaultButton(QMessageBox.StandardButton.Cancel)
|
||||
info.exec()
|
||||
|
||||
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
||||
"""Rescan track"""
|
||||
|
||||
self.data_model.mark_unplayed(row_numbers)
|
||||
self.playlist_model.mark_unplayed(row_numbers)
|
||||
self.clear_selection()
|
||||
|
||||
def _rescan(self, row_number: int) -> None:
|
||||
"""Rescan track"""
|
||||
|
||||
self.data_model.rescan_track(row_number)
|
||||
self.playlist_model.rescan_track(row_number)
|
||||
self.clear_selection()
|
||||
|
||||
def scroll_to_top(self, row_number: int) -> None:
|
||||
@ -591,62 +653,36 @@ class PlaylistTab(QTableView):
|
||||
# We need to be in MultiSelection mode
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
||||
# Get the duplicate rows
|
||||
duplicate_rows = self.data_model.get_duplicate_rows()
|
||||
duplicate_rows = self.playlist_model.get_duplicate_rows()
|
||||
# Select the rows
|
||||
for duplicate_row in duplicate_rows:
|
||||
self.selectRow(duplicate_row)
|
||||
# Reset selection mode
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
def selected_model_row_number(self) -> Optional[int]:
|
||||
def selectionChanged(
|
||||
self, selected: QItemSelection, deselected: QItemSelection
|
||||
) -> None:
|
||||
"""
|
||||
Return the model row number corresponding to the selected row or None
|
||||
Toggle drag behaviour according to whether rows are selected
|
||||
"""
|
||||
|
||||
selected_index = self._selected_row_index()
|
||||
if selected_index is None:
|
||||
return None
|
||||
if hasattr(self.proxy_model, "mapToSource"):
|
||||
return self.proxy_model.mapToSource(selected_index).row()
|
||||
return selected_index.row()
|
||||
|
||||
def selected_model_row_numbers(self) -> List[int]:
|
||||
"""
|
||||
Return a list of model row numbers corresponding to the selected rows or
|
||||
an empty list.
|
||||
"""
|
||||
|
||||
selected_indexes = self._selected_row_indexes()
|
||||
if selected_indexes is None:
|
||||
return []
|
||||
if hasattr(self.proxy_model, "mapToSource"):
|
||||
return [self.proxy_model.mapToSource(a).row() for a in selected_indexes]
|
||||
return [a.row() for a in selected_indexes]
|
||||
|
||||
def _selected_row_index(self) -> Optional[QModelIndex]:
|
||||
"""
|
||||
Return the selected row index or None if none selected.
|
||||
"""
|
||||
|
||||
row_indexes = self._selected_row_indexes()
|
||||
|
||||
if len(row_indexes) != 1:
|
||||
show_warning(
|
||||
self.musicmuster, "No or multiple rows selected", "Select only one row"
|
||||
selected_rows = self.get_selected_rows()
|
||||
# If no rows are selected, we have nothing to do
|
||||
if len(selected_rows) == 0:
|
||||
self.musicmuster.lblSumPlaytime.setText("")
|
||||
else:
|
||||
selected_duration = self.playlist_model.get_rows_duration(
|
||||
self.get_selected_rows()
|
||||
)
|
||||
return None
|
||||
if selected_duration > 0:
|
||||
self.musicmuster.lblSumPlaytime.setText(
|
||||
f"Selected duration: {ms_to_mmss(selected_duration)}"
|
||||
)
|
||||
else:
|
||||
self.musicmuster.lblSumPlaytime.setText("")
|
||||
|
||||
return row_indexes[0]
|
||||
|
||||
def _selected_row_indexes(self) -> List[QModelIndex]:
|
||||
"""
|
||||
Return a list of indexes of column 1 of selected rows
|
||||
"""
|
||||
|
||||
sm = self.selectionModel()
|
||||
if sm and sm.hasSelection():
|
||||
return sm.selectedRows()
|
||||
return []
|
||||
super().selectionChanged(selected, deselected)
|
||||
|
||||
def _set_column_widths(self) -> None:
|
||||
"""Column widths from settings"""
|
||||
@ -668,17 +704,6 @@ class PlaylistTab(QTableView):
|
||||
else:
|
||||
self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH)
|
||||
|
||||
def set_row_as_next_track(self) -> None:
|
||||
"""
|
||||
Set selected row as next track
|
||||
"""
|
||||
|
||||
model_row_number = self.selected_model_row_number()
|
||||
if model_row_number is None:
|
||||
return
|
||||
self.data_model.set_next_row(model_row_number)
|
||||
self.clearSelection()
|
||||
|
||||
def _span_cells(self, row: int, column: int, rowSpan: int, columnSpan: int) -> None:
|
||||
"""
|
||||
Implement spanning of cells, initiated by signal
|
||||
@ -686,7 +711,9 @@ class PlaylistTab(QTableView):
|
||||
|
||||
model = self.proxy_model
|
||||
if hasattr(model, "mapToSource"):
|
||||
edit_index = model.mapFromSource(self.data_model.createIndex(row, column))
|
||||
edit_index = model.mapFromSource(
|
||||
self.playlist_model.createIndex(row, column)
|
||||
)
|
||||
row = edit_index.row()
|
||||
column = edit_index.column()
|
||||
|
||||
@ -704,5 +731,5 @@ class PlaylistTab(QTableView):
|
||||
def _unmark_as_next(self) -> None:
|
||||
"""Rescan track"""
|
||||
|
||||
self.data_model.set_next_row(None)
|
||||
self.playlist_model.set_next_row(None)
|
||||
self.clear_selection()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# #!/usr/bin/env python
|
||||
#
|
||||
import os
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ def test_get_relative_date():
|
||||
assert get_relative_date(None) == "Never"
|
||||
today_at_10 = datetime.now().replace(hour=10, minute=0)
|
||||
today_at_11 = datetime.now().replace(hour=11, minute=0)
|
||||
assert get_relative_date(today_at_10, today_at_11) == "Today 10:00"
|
||||
assert get_relative_date(today_at_10, today_at_11) == "10:00"
|
||||
eight_days_ago = today_at_10 - timedelta(days=8)
|
||||
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
||||
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user