V3 tweaks and polishes
This commit is contained in:
parent
63a38b5bf9
commit
3179c6f5de
@ -104,10 +104,6 @@ class PlaylistTrack:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
Only initialises data structure. Call set_plr to populate.
|
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.artist: Optional[str] = None
|
||||||
|
|||||||
@ -32,16 +32,6 @@ class Config(object):
|
|||||||
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
||||||
COLOUR_UNREADABLE = "#dc3545"
|
COLOUR_UNREADABLE = "#dc3545"
|
||||||
COLOUR_WARNING_TIMER = "#ffc107"
|
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
|
DBFS_SILENCE = -50
|
||||||
DEBUG_FUNCTIONS: List[Optional[str]] = []
|
DEBUG_FUNCTIONS: List[Optional[str]] = []
|
||||||
DEBUG_MODULES: List[Optional[str]] = ["dbconfig"]
|
DEBUG_MODULES: List[Optional[str]] = ["dbconfig"]
|
||||||
|
|||||||
@ -31,9 +31,7 @@ def Session() -> Generator[scoped_session, None, None]:
|
|||||||
function = frame.function
|
function = frame.function
|
||||||
lineno = frame.lineno
|
lineno = frame.lineno
|
||||||
Session = scoped_session(sessionmaker(bind=engine))
|
Session = scoped_session(sessionmaker(bind=engine))
|
||||||
log.debug(
|
log.debug(f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]")
|
||||||
f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]"
|
|
||||||
)
|
|
||||||
yield Session
|
yield Session
|
||||||
log.debug(f" Session released [{hex(id(Session))}]")
|
log.debug(f" Session released [{hex(id(Session))}]")
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|||||||
@ -87,7 +87,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
default_yes=True,
|
default_yes=True,
|
||||||
):
|
):
|
||||||
move_existing = 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:
|
if move_existing:
|
||||||
self.model.move_track_to_header(self.new_row_number, existing_prd, note)
|
self.model.move_track_to_header(self.new_row_number, existing_prd, note)
|
||||||
else:
|
else:
|
||||||
@ -95,7 +95,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
# Close dialog - we can only add one track to a header
|
# Close dialog - we can only add one track to a header
|
||||||
self.accept()
|
self.accept()
|
||||||
else:
|
else:
|
||||||
if move_existing and existing_prd: # and existing_prd for mypy's benefit
|
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
||||||
self.model.move_track_add_note(self.new_row_number, existing_prd, note)
|
self.model.move_track_add_note(self.new_row_number, existing_prd, note)
|
||||||
else:
|
else:
|
||||||
self.model.insert_row(self.new_row_number, track_id, note)
|
self.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 mutagen.mp3 import MP3 # type: ignore
|
||||||
from pydub import AudioSegment, effects
|
from pydub import AudioSegment, effects
|
||||||
from pydub.utils import mediainfo
|
from pydub.utils import mediainfo
|
||||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
|
from PyQt6.QtWidgets import QMainWindow, QMessageBox
|
||||||
from tinytag import TinyTag # type: ignore
|
from tinytag import TinyTag # type: ignore
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
@ -99,7 +99,7 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
|||||||
|
|
||||||
|
|
||||||
def get_embedded_time(text: str) -> Optional[datetime]:
|
def get_embedded_time(text: str) -> Optional[datetime]:
|
||||||
"""Return datetime specified as @hh:mm:ss in text"""
|
"""Return datetime specified as @hh:mm in text"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
match = start_time_re.search(text)
|
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)
|
weeks, days = divmod((reference_date.date() - past_date.date()).days, 7)
|
||||||
if weeks == days == 0:
|
if weeks == days == 0:
|
||||||
# Same day so return time instead
|
# Same day so return time instead
|
||||||
return past_date.strftime("%H:%M")
|
return Config.LAST_PLAYED_TODAY_STRING + " " + past_date.strftime("%H:%M")
|
||||||
if weeks == 1:
|
if weeks == 1:
|
||||||
weeks_str = "week"
|
weeks_str = "week"
|
||||||
else:
|
else:
|
||||||
@ -226,32 +226,6 @@ def leading_silence(
|
|||||||
return min(trim_ms, len(audio_segment))
|
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(
|
def ms_to_mmss(
|
||||||
ms: Optional[int],
|
ms: Optional[int],
|
||||||
decimals: int = 0,
|
decimals: int = 0,
|
||||||
@ -390,6 +364,32 @@ def open_in_audacity(path: str) -> bool:
|
|||||||
return True
|
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):
|
def set_track_metadata(track):
|
||||||
"""Set/update track metadata in database"""
|
"""Set/update track metadata in database"""
|
||||||
|
|
||||||
|
|||||||
@ -307,8 +307,7 @@ class Playlists(Base):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
select(cls).where(cls.open.is_(True))
|
select(cls).where(cls.open.is_(True)).order_by(cls.tab)
|
||||||
.order_by(cls.tab)
|
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def mark_open(self) -> None:
|
def mark_open(self) -> None:
|
||||||
@ -323,10 +322,10 @@ class Playlists(Base):
|
|||||||
Return True if no playlist of this name exists else false.
|
Return True if no playlist of this name exists else false.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return session.execute(
|
return (
|
||||||
select(Playlists)
|
session.execute(select(Playlists).where(Playlists.name == name)).first()
|
||||||
.where(Playlists.name == name)
|
is None
|
||||||
).first() is None
|
)
|
||||||
|
|
||||||
def rename(self, session: scoped_session, new_name: str) -> None:
|
def rename(self, session: scoped_session, new_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -479,9 +478,7 @@ class PlaylistRows(Base):
|
|||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_row(
|
def delete_row(session: scoped_session, playlist_id: int, row_number: int) -> None:
|
||||||
session: scoped_session, playlist_id: int, row_number: int
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Delete passed row in given playlist.
|
Delete passed row in given playlist.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
# import os
|
|
||||||
import threading
|
import threading
|
||||||
import vlc # type: ignore
|
import vlc # type: ignore
|
||||||
|
|
||||||
#
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from helpers import file_is_unreadable
|
from helpers import file_is_unreadable
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -10,7 +8,7 @@ from time import sleep
|
|||||||
|
|
||||||
from log import log
|
from log import log
|
||||||
|
|
||||||
from PyQt6.QtCore import ( # type: ignore
|
from PyQt6.QtCore import (
|
||||||
QRunnable,
|
QRunnable,
|
||||||
QThreadPool,
|
QThreadPool,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -170,7 +170,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if plr:
|
if plr:
|
||||||
# Add track to PlaylistRows
|
# Add track to PlaylistRows
|
||||||
plr.track_id = track_id
|
plr.track_id = track_id
|
||||||
# Add any further note
|
# Add any further note (header will already have a note)
|
||||||
if note:
|
if note:
|
||||||
plr.note += "\n" + note
|
plr.note += "\n" + note
|
||||||
# Reset header row spanning
|
# Reset header row spanning
|
||||||
@ -262,9 +262,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Check for OBS scene change
|
# Check for OBS scene change
|
||||||
self.obs_scene_change(row_number)
|
self.obs_scene_change(row_number)
|
||||||
|
|
||||||
# Update Playdates in database
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
|
# Update Playdates in database
|
||||||
Playdates(session, track_sequence.now.track_id)
|
Playdates(session, track_sequence.now.track_id)
|
||||||
|
|
||||||
|
# Mark track as played in playlist
|
||||||
plr = session.get(PlaylistRows, track_sequence.now.plr_id)
|
plr = session.get(PlaylistRows, track_sequence.now.plr_id)
|
||||||
if plr:
|
if plr:
|
||||||
plr.played = True
|
plr.played = True
|
||||||
@ -305,32 +307,29 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
prd = self.playlist_rows[row]
|
prd = self.playlist_rows[row]
|
||||||
|
|
||||||
# Dispatch to role-specific functions
|
# Dispatch to role-specific functions
|
||||||
if role == Qt.ItemDataRole.DisplayRole:
|
dispatch_table = {
|
||||||
return self.display_role(row, column, prd)
|
int(Qt.ItemDataRole.DisplayRole): self.display_role,
|
||||||
elif role == Qt.ItemDataRole.DecorationRole:
|
int(Qt.ItemDataRole.EditRole): self.edit_role,
|
||||||
pass
|
int(Qt.ItemDataRole.FontRole): self.font_role,
|
||||||
elif role == Qt.ItemDataRole.EditRole:
|
int(Qt.ItemDataRole.BackgroundRole): self.background_role,
|
||||||
return self.edit_role(row, column, prd)
|
}
|
||||||
elif role == Qt.ItemDataRole.ToolTipRole:
|
|
||||||
pass
|
if role in dispatch_table:
|
||||||
elif role == Qt.ItemDataRole.StatusTipRole:
|
return dispatch_table[role](row, column, prd)
|
||||||
pass
|
|
||||||
elif role == Qt.ItemDataRole.WhatsThisRole:
|
# Document other roles but don't use them
|
||||||
pass
|
if role in [
|
||||||
elif role == Qt.ItemDataRole.SizeHintRole:
|
Qt.ItemDataRole.DecorationRole,
|
||||||
pass
|
Qt.ItemDataRole.ToolTipRole,
|
||||||
elif role == Qt.ItemDataRole.FontRole:
|
Qt.ItemDataRole.StatusTipRole,
|
||||||
return self.font_role(row, column, prd)
|
Qt.ItemDataRole.WhatsThisRole,
|
||||||
elif role == Qt.ItemDataRole.TextAlignmentRole:
|
Qt.ItemDataRole.SizeHintRole,
|
||||||
pass
|
Qt.ItemDataRole.TextAlignmentRole,
|
||||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
Qt.ItemDataRole.ForegroundRole,
|
||||||
return self.background_role(row, column, prd)
|
Qt.ItemDataRole.CheckStateRole,
|
||||||
elif role == Qt.ItemDataRole.ForegroundRole:
|
Qt.ItemDataRole.InitialSortOrderRole,
|
||||||
pass
|
]:
|
||||||
elif role == Qt.ItemDataRole.CheckStateRole:
|
return QVariant()
|
||||||
pass
|
|
||||||
elif role == Qt.ItemDataRole.InitialSortOrderRole:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fall through to no-op
|
# Fall through to no-op
|
||||||
return QVariant()
|
return QVariant()
|
||||||
@ -363,36 +362,39 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals.span_cells_signal.emit(
|
self.signals.span_cells_signal.emit(
|
||||||
row, HEADER_NOTES_COLUMN, 1, self.columnCount() - 1
|
row, HEADER_NOTES_COLUMN, 1, self.columnCount() - 1
|
||||||
)
|
)
|
||||||
return QVariant(self.header_text(prd))
|
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))
|
||||||
else:
|
else:
|
||||||
return QVariant()
|
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 column == Col.START_TIME.value:
|
||||||
if row in self.start_end_times:
|
if row in self.start_end_times:
|
||||||
start_time = self.start_end_times[row].start_time
|
start_time = self.start_end_times[row].start_time
|
||||||
if start_time:
|
if start_time:
|
||||||
return QVariant(start_time.strftime(Config.TRACK_TIME_FORMAT))
|
return QVariant(start_time.strftime(Config.TRACK_TIME_FORMAT))
|
||||||
return QVariant()
|
return QVariant()
|
||||||
|
|
||||||
if column == Col.END_TIME.value:
|
if column == Col.END_TIME.value:
|
||||||
if row in self.start_end_times:
|
if row in self.start_end_times:
|
||||||
end_time = self.start_end_times[row].end_time
|
end_time = self.start_end_times[row].end_time
|
||||||
if end_time:
|
if end_time:
|
||||||
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
|
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
|
||||||
return QVariant()
|
return QVariant()
|
||||||
if column == Col.LAST_PLAYED.value:
|
|
||||||
return QVariant(get_relative_date(prd.lastplayed))
|
dispatch_table = {
|
||||||
if column == Col.BITRATE.value:
|
Col.START_GAP.value: QVariant(prd.start_gap),
|
||||||
return QVariant(prd.bitrate)
|
Col.TITLE.value: QVariant(prd.title),
|
||||||
if column == Col.NOTE.value:
|
Col.ARTIST.value: QVariant(prd.artist),
|
||||||
return QVariant(prd.note)
|
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]
|
||||||
|
|
||||||
return QVariant()
|
return QVariant()
|
||||||
|
|
||||||
@ -408,26 +410,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
super().endResetModel()
|
super().endResetModel()
|
||||||
self.row_order_changed(self.playlist_id)
|
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:
|
def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
||||||
"""
|
"""
|
||||||
Return text for editing
|
Return text for editing
|
||||||
@ -479,6 +461,26 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return QVariant(boldfont)
|
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:
|
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
|
||||||
"""
|
"""
|
||||||
Sanitises proposed new row number.
|
Sanitises proposed new row number.
|
||||||
@ -498,6 +500,13 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return new_row_number
|
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:
|
def get_row_track_path(self, row_number: int) -> str:
|
||||||
"""
|
"""
|
||||||
Return path of track associated with row or empty string if no track associated
|
Return path of track associated with row or empty string if no track associated
|
||||||
@ -516,13 +525,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return duration
|
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]:
|
def get_unplayed_rows(self) -> List[int]:
|
||||||
"""
|
"""
|
||||||
Return a list of unplayed row numbers
|
Return a list of unplayed row numbers
|
||||||
@ -677,22 +679,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if self.is_played_row(row_number):
|
if self.is_played_row(row_number):
|
||||||
self.invalidate_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(
|
def insert_row(
|
||||||
self,
|
self,
|
||||||
proposed_row_number: Optional[int],
|
proposed_row_number: Optional[int],
|
||||||
@ -737,6 +723,22 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
for modified_row in modified_rows:
|
for modified_row in modified_rows:
|
||||||
self.invalidate_row(modified_row)
|
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]:
|
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
||||||
"""
|
"""
|
||||||
If this track_id is in the playlist, return the PlaylistRowData object
|
If this track_id is in the playlist, return the PlaylistRowData object
|
||||||
@ -749,21 +751,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
|
||||||
"""
|
|
||||||
Mark row as unplayed
|
|
||||||
"""
|
|
||||||
|
|
||||||
with Session() as session:
|
|
||||||
for row_number in row_numbers:
|
|
||||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
|
||||||
if not plr:
|
|
||||||
return
|
|
||||||
plr.played = False
|
|
||||||
self.refresh_row(session, row_number)
|
|
||||||
|
|
||||||
self.invalidate_rows(row_numbers)
|
|
||||||
|
|
||||||
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Move the playlist rows given to to_row and below.
|
Move the playlist rows given to to_row and below.
|
||||||
@ -829,6 +816,21 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals.row_order_changed_signal.emit(self.playlist_id)
|
self.signals.row_order_changed_signal.emit(self.playlist_id)
|
||||||
self.invalidate_rows(list(row_map.keys()))
|
self.invalidate_rows(list(row_map.keys()))
|
||||||
|
|
||||||
|
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Mark row as unplayed
|
||||||
|
"""
|
||||||
|
|
||||||
|
with Session() as session:
|
||||||
|
for row_number in row_numbers:
|
||||||
|
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||||
|
if not plr:
|
||||||
|
return
|
||||||
|
plr.played = False
|
||||||
|
self.refresh_row(session, row_number)
|
||||||
|
|
||||||
|
self.invalidate_rows(row_numbers)
|
||||||
|
|
||||||
def move_rows_between_playlists(
|
def move_rows_between_playlists(
|
||||||
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -969,7 +971,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Actions required:
|
Actions required:
|
||||||
- sanity check
|
- sanity check
|
||||||
- update display
|
- update display
|
||||||
- update track times
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sanity check
|
# Sanity check
|
||||||
@ -1124,7 +1125,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals.next_track_changed_signal.emit()
|
self.signals.next_track_changed_signal.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update playing_trtack
|
# Update playing_track
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
track_sequence.next = PlaylistTrack()
|
track_sequence.next = PlaylistTrack()
|
||||||
try:
|
try:
|
||||||
@ -1331,14 +1332,14 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
playlist_model: PlaylistModel,
|
data_model: PlaylistModel,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.playlist_model = playlist_model
|
self.data_model = data_model
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.setSourceModel(playlist_model)
|
self.setSourceModel(data_model)
|
||||||
# Search all columns
|
# Search all columns
|
||||||
self.setFilterKeyColumn(-1)
|
self.setFilterKeyColumn(-1)
|
||||||
|
|
||||||
@ -1347,8 +1348,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
Subclass to filter by played status
|
Subclass to filter by played status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.playlist_model.played_tracks_hidden:
|
if self.data_model.played_tracks_hidden:
|
||||||
if self.playlist_model.is_played_row(source_row):
|
if self.data_model.is_played_row(source_row):
|
||||||
# Don't hide current or next track
|
# Don't hide current or next track
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
if track_sequence.next.plr_id:
|
if track_sequence.next.plr_id:
|
||||||
@ -1356,7 +1357,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
if (
|
if (
|
||||||
next_plr
|
next_plr
|
||||||
and next_plr.plr_rownum == source_row
|
and next_plr.plr_rownum == source_row
|
||||||
and next_plr.playlist_id == self.playlist_model.playlist_id
|
and next_plr.playlist_id == self.data_model.playlist_id
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
if track_sequence.now.plr_id:
|
if track_sequence.now.plr_id:
|
||||||
@ -1386,25 +1387,25 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
# ######################################
|
# ######################################
|
||||||
|
|
||||||
def current_track_started(self):
|
def current_track_started(self):
|
||||||
return self.playlist_model.current_track_started()
|
return self.data_model.current_track_started()
|
||||||
|
|
||||||
def delete_rows(self, row_numbers: List[int]) -> None:
|
def delete_rows(self, row_numbers: List[int]) -> None:
|
||||||
return self.playlist_model.delete_rows(row_numbers)
|
return self.data_model.delete_rows(row_numbers)
|
||||||
|
|
||||||
def get_duplicate_rows(self) -> List[int]:
|
def get_duplicate_rows(self) -> List[int]:
|
||||||
return self.playlist_model.get_duplicate_rows()
|
return self.data_model.get_duplicate_rows()
|
||||||
|
|
||||||
def get_rows_duration(self, row_numbers: List[int]) -> int:
|
def get_rows_duration(self, row_numbers: List[int]) -> int:
|
||||||
return self.playlist_model.get_rows_duration(row_numbers)
|
return self.data_model.get_rows_duration(row_numbers)
|
||||||
|
|
||||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||||
return self.playlist_model.get_row_info(row_number)
|
return self.data_model.get_row_info(row_number)
|
||||||
|
|
||||||
def get_row_track_path(self, row_number: int) -> str:
|
def get_row_track_path(self, row_number: int) -> str:
|
||||||
return self.playlist_model.get_row_track_path(row_number)
|
return self.data_model.get_row_track_path(row_number)
|
||||||
|
|
||||||
def hide_played_tracks(self, hide: bool) -> None:
|
def hide_played_tracks(self, hide: bool) -> None:
|
||||||
return self.playlist_model.hide_played_tracks(hide)
|
return self.data_model.hide_played_tracks(hide)
|
||||||
|
|
||||||
def insert_row(
|
def insert_row(
|
||||||
self,
|
self,
|
||||||
@ -1412,70 +1413,68 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
track_id: Optional[int] = None,
|
track_id: Optional[int] = None,
|
||||||
note: Optional[str] = None,
|
note: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.playlist_model.insert_row(proposed_row_number, track_id, note)
|
return self.data_model.insert_row(proposed_row_number, track_id, note)
|
||||||
|
|
||||||
def is_header_row(self, row_number: int) -> bool:
|
def is_header_row(self, row_number: int) -> bool:
|
||||||
return self.playlist_model.is_header_row(row_number)
|
return self.data_model.is_header_row(row_number)
|
||||||
|
|
||||||
def is_played_row(self, row_number: int) -> bool:
|
def is_played_row(self, row_number: int) -> bool:
|
||||||
return self.playlist_model.is_played_row(row_number)
|
return self.data_model.is_played_row(row_number)
|
||||||
|
|
||||||
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
|
||||||
return self.playlist_model.is_track_in_playlist(track_id)
|
return self.data_model.is_track_in_playlist(track_id)
|
||||||
|
|
||||||
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
return self.playlist_model.mark_unplayed(row_numbers)
|
return self.data_model.mark_unplayed(row_numbers)
|
||||||
|
|
||||||
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
||||||
return self.playlist_model.move_rows(from_rows, to_row_number)
|
return self.data_model.move_rows(from_rows, to_row_number)
|
||||||
|
|
||||||
def move_rows_between_playlists(
|
def move_rows_between_playlists(
|
||||||
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.playlist_model.move_rows_between_playlists(
|
return self.data_model.move_rows_between_playlists(
|
||||||
from_rows, to_row_number, to_playlist_id
|
from_rows, to_row_number, to_playlist_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def move_track_add_note(
|
def move_track_add_note(
|
||||||
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
|
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.playlist_model.move_track_add_note(
|
return self.data_model.move_track_add_note(new_row_number, existing_prd, note)
|
||||||
new_row_number, existing_prd, note
|
|
||||||
)
|
|
||||||
|
|
||||||
def move_track_to_header(
|
def move_track_to_header(
|
||||||
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
|
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
return self.playlist_model.move_track_to_header(
|
return self.data_model.move_track_to_header(
|
||||||
header_row_number, existing_prd, note
|
header_row_number, existing_prd, note
|
||||||
)
|
)
|
||||||
|
|
||||||
def open_in_audacity(self, row_number: int) -> None:
|
def open_in_audacity(self, row_number: int) -> None:
|
||||||
return self.playlist_model.open_in_audacity(row_number)
|
return self.data_model.open_in_audacity(row_number)
|
||||||
|
|
||||||
def previous_track_ended(self) -> None:
|
def previous_track_ended(self) -> None:
|
||||||
return self.playlist_model.previous_track_ended()
|
return self.data_model.previous_track_ended()
|
||||||
|
|
||||||
def remove_track(self, row_number: int) -> None:
|
def remove_track(self, row_number: int) -> None:
|
||||||
return self.playlist_model.remove_track(row_number)
|
return self.data_model.remove_track(row_number)
|
||||||
|
|
||||||
def rescan_track(self, row_number: int) -> None:
|
def rescan_track(self, row_number: int) -> None:
|
||||||
return self.playlist_model.rescan_track(row_number)
|
return self.data_model.rescan_track(row_number)
|
||||||
|
|
||||||
def set_next_row(self, row_number: Optional[int]) -> None:
|
def set_next_row(self, row_number: Optional[int]) -> None:
|
||||||
return self.playlist_model.set_next_row(row_number)
|
return self.data_model.set_next_row(row_number)
|
||||||
|
|
||||||
def sort_by_artist(self, row_numbers: List[int]) -> None:
|
def sort_by_artist(self, row_numbers: List[int]) -> None:
|
||||||
return self.playlist_model.sort_by_artist(row_numbers)
|
return self.data_model.sort_by_artist(row_numbers)
|
||||||
|
|
||||||
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
||||||
return self.playlist_model.sort_by_duration(row_numbers)
|
return self.data_model.sort_by_duration(row_numbers)
|
||||||
|
|
||||||
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
||||||
return self.playlist_model.sort_by_lastplayed(row_numbers)
|
return self.data_model.sort_by_lastplayed(row_numbers)
|
||||||
|
|
||||||
def sort_by_title(self, row_numbers: List[int]) -> None:
|
def sort_by_title(self, row_numbers: List[int]) -> None:
|
||||||
return self.playlist_model.sort_by_title(row_numbers)
|
return self.data_model.sort_by_title(row_numbers)
|
||||||
|
|
||||||
def update_track_times(self) -> None:
|
def update_track_times(self) -> None:
|
||||||
return self.playlist_model.update_track_times()
|
return self.data_model.update_track_times()
|
||||||
|
|||||||
299
app/playlists.py
299
app/playlists.py
@ -34,6 +34,7 @@ from config import Config
|
|||||||
from helpers import (
|
from helpers import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
|
show_OK,
|
||||||
show_warning,
|
show_warning,
|
||||||
)
|
)
|
||||||
from models import Settings
|
from models import Settings
|
||||||
@ -50,9 +51,9 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
- checks with user before abandoning edit on Escape
|
- checks with user before abandoning edit on Escape
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent, playlist_model: PlaylistModel) -> None:
|
def __init__(self, parent, data_model: PlaylistModel) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.playlist_model = playlist_model
|
self.data_model = data_model
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
|
|
||||||
def createEditor(
|
def createEditor(
|
||||||
@ -113,7 +114,7 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
else:
|
else:
|
||||||
edit_index = index
|
edit_index = index
|
||||||
|
|
||||||
value = self.playlist_model.data(edit_index, Qt.ItemDataRole.EditRole)
|
value = self.data_model.data(edit_index, Qt.ItemDataRole.EditRole)
|
||||||
editor.setPlainText(value.value())
|
editor.setPlainText(value.value())
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
def setModelData(self, editor, model, index):
|
||||||
@ -124,7 +125,7 @@ class EscapeDelegate(QStyledItemDelegate):
|
|||||||
edit_index = index
|
edit_index = index
|
||||||
|
|
||||||
value = editor.toPlainText().strip()
|
value = editor.toPlainText().strip()
|
||||||
self.playlist_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
self.data_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
|
||||||
|
|
||||||
def updateEditorGeometry(self, editor, option, index):
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
editor.setGeometry(option.rect)
|
editor.setGeometry(option.rect)
|
||||||
@ -149,6 +150,10 @@ class PlaylistStyle(QProxyStyle):
|
|||||||
|
|
||||||
|
|
||||||
class PlaylistTab(QTableView):
|
class PlaylistTab(QTableView):
|
||||||
|
"""
|
||||||
|
The playlist view
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
musicmuster: "Window",
|
musicmuster: "Window",
|
||||||
@ -161,9 +166,9 @@ class PlaylistTab(QTableView):
|
|||||||
self.playlist_id = playlist_id
|
self.playlist_id = playlist_id
|
||||||
|
|
||||||
# Set up widget
|
# Set up widget
|
||||||
self.playlist_model = PlaylistModel(playlist_id)
|
self.data_model = PlaylistModel(playlist_id)
|
||||||
self.proxy_model = PlaylistProxyModel(self.playlist_model)
|
self.proxy_model = PlaylistProxyModel(self.data_model)
|
||||||
self.setItemDelegate(EscapeDelegate(self, self.playlist_model))
|
self.setItemDelegate(EscapeDelegate(self, self.data_model))
|
||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||||
@ -190,10 +195,6 @@ class PlaylistTab(QTableView):
|
|||||||
self.signals.resize_rows_signal.connect(self.resizeRowsToContents)
|
self.signals.resize_rows_signal.connect(self.resizeRowsToContents)
|
||||||
self.signals.span_cells_signal.connect(self._span_cells)
|
self.signals.span_cells_signal.connect(self._span_cells)
|
||||||
|
|
||||||
# Initialise miscellaneous instance variables
|
|
||||||
self.search_text: str = ""
|
|
||||||
self.sort_undo: List[int] = []
|
|
||||||
|
|
||||||
# Selection model
|
# Selection model
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
@ -202,6 +203,8 @@ class PlaylistTab(QTableView):
|
|||||||
self.setModel(self.proxy_model)
|
self.setModel(self.proxy_model)
|
||||||
self._set_column_widths()
|
self._set_column_widths()
|
||||||
|
|
||||||
|
# ########## Overrident class functions ##########
|
||||||
|
|
||||||
def closeEditor(
|
def closeEditor(
|
||||||
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
|
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -220,7 +223,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Update start times in case a start time in a note has been
|
# Update start times in case a start time in a note has been
|
||||||
# edited
|
# edited
|
||||||
self.playlist_model.update_track_times()
|
self.data_model.update_track_times()
|
||||||
|
|
||||||
def dropEvent(self, event):
|
def dropEvent(self, event):
|
||||||
if event.source() is not self or (
|
if event.source() is not self or (
|
||||||
@ -240,6 +243,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Reset drag mode to allow row selection by dragging
|
# Reset drag mode to allow row selection by dragging
|
||||||
self.setDragEnabled(False)
|
self.setDragEnabled(False)
|
||||||
|
|
||||||
# Deselect rows
|
# Deselect rows
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
@ -252,7 +256,7 @@ class PlaylistTab(QTableView):
|
|||||||
event: Optional[QEvent],
|
event: Optional[QEvent],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Override PySide2.QAbstractItemView.edit to catch when editing starts
|
Override QAbstractItemView.edit to catch when editing starts
|
||||||
|
|
||||||
Editing only ever starts with a double click on a cell
|
Editing only ever starts with a double click on a cell
|
||||||
"""
|
"""
|
||||||
@ -264,6 +268,43 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
return result
|
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(
|
def _add_context_menu(
|
||||||
self,
|
self,
|
||||||
text: str,
|
text: str,
|
||||||
@ -286,121 +327,6 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
return menu_item
|
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:
|
def _add_track(self) -> None:
|
||||||
"""Add a track to a section header making it a normal track row"""
|
"""Add a track to a section header making it a normal track row"""
|
||||||
|
|
||||||
@ -412,7 +338,7 @@ class PlaylistTab(QTableView):
|
|||||||
dlg = TrackSelectDialog(
|
dlg = TrackSelectDialog(
|
||||||
session=session,
|
session=session,
|
||||||
new_row_number=model_row_number,
|
new_row_number=model_row_number,
|
||||||
model=self.playlist_model,
|
model=self.data_model,
|
||||||
add_to_header=True,
|
add_to_header=True,
|
||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
@ -516,6 +442,12 @@ class PlaylistTab(QTableView):
|
|||||||
"Copy track path", lambda: self._copy_path(model_row_number)
|
"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:
|
def _column_resize(self, column_number: int, _old: int, _new: int) -> None:
|
||||||
"""
|
"""
|
||||||
Called when column width changes. Save new width to database.
|
Called when column width changes. Save new width to database.
|
||||||
@ -546,7 +478,7 @@ class PlaylistTab(QTableView):
|
|||||||
to the clipboard. Otherwise, return None.
|
to the clipboard. Otherwise, return None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
track_path = self.playlist_model.get_row_info(row_number).path
|
track_path = self.data_model.get_row_info(row_number).path
|
||||||
if not track_path:
|
if not track_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -583,9 +515,20 @@ class PlaylistTab(QTableView):
|
|||||||
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.playlist_model.delete_rows(self.selected_model_row_numbers())
|
self.data_model.delete_rows(self.selected_model_row_numbers())
|
||||||
self.clear_selection()
|
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]:
|
def get_selected_rows(self) -> List[int]:
|
||||||
"""Return a list of selected row numbers sorted by row"""
|
"""Return a list of selected row numbers sorted by row"""
|
||||||
|
|
||||||
@ -596,7 +539,7 @@ class PlaylistTab(QTableView):
|
|||||||
def _info_row(self, row_number: int) -> None:
|
def _info_row(self, row_number: int) -> None:
|
||||||
"""Display popup with info re row"""
|
"""Display popup with info re row"""
|
||||||
|
|
||||||
prd = self.playlist_model.get_row_info(row_number)
|
prd = self.data_model.get_row_info(row_number)
|
||||||
if prd:
|
if prd:
|
||||||
txt = (
|
txt = (
|
||||||
f"Title: {prd.title}\n"
|
f"Title: {prd.title}\n"
|
||||||
@ -610,23 +553,18 @@ class PlaylistTab(QTableView):
|
|||||||
else:
|
else:
|
||||||
txt = f"Can't find info about row{row_number}"
|
txt = f"Can't find info about row{row_number}"
|
||||||
|
|
||||||
info: QMessageBox = QMessageBox(self)
|
show_OK(self.musicmuster, "Track info", txt)
|
||||||
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:
|
def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.playlist_model.mark_unplayed(row_numbers)
|
self.data_model.mark_unplayed(row_numbers)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def _rescan(self, row_number: int) -> None:
|
def _rescan(self, row_number: int) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.playlist_model.rescan_track(row_number)
|
self.data_model.rescan_track(row_number)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def scroll_to_top(self, row_number: int) -> None:
|
def scroll_to_top(self, row_number: int) -> None:
|
||||||
@ -653,36 +591,62 @@ class PlaylistTab(QTableView):
|
|||||||
# We need to be in MultiSelection mode
|
# We need to be in MultiSelection mode
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
|
||||||
# Get the duplicate rows
|
# Get the duplicate rows
|
||||||
duplicate_rows = self.playlist_model.get_duplicate_rows()
|
duplicate_rows = self.data_model.get_duplicate_rows()
|
||||||
# Select the rows
|
# Select the rows
|
||||||
for duplicate_row in duplicate_rows:
|
for duplicate_row in duplicate_rows:
|
||||||
self.selectRow(duplicate_row)
|
self.selectRow(duplicate_row)
|
||||||
# Reset selection mode
|
# Reset selection mode
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
|
||||||
def selectionChanged(
|
def selected_model_row_number(self) -> Optional[int]:
|
||||||
self, selected: QItemSelection, deselected: QItemSelection
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Toggle drag behaviour according to whether rows are selected
|
Return the model row number corresponding to the selected row or None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
selected_rows = self.get_selected_rows()
|
selected_index = self._selected_row_index()
|
||||||
# If no rows are selected, we have nothing to do
|
if selected_index is None:
|
||||||
if len(selected_rows) == 0:
|
return None
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
if hasattr(self.proxy_model, "mapToSource"):
|
||||||
else:
|
return self.proxy_model.mapToSource(selected_index).row()
|
||||||
selected_duration = self.playlist_model.get_rows_duration(
|
return selected_index.row()
|
||||||
self.get_selected_rows()
|
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
if selected_duration > 0:
|
return None
|
||||||
self.musicmuster.lblSumPlaytime.setText(
|
|
||||||
f"Selected duration: {ms_to_mmss(selected_duration)}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
|
||||||
|
|
||||||
super().selectionChanged(selected, deselected)
|
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 _set_column_widths(self) -> None:
|
def _set_column_widths(self) -> None:
|
||||||
"""Column widths from settings"""
|
"""Column widths from settings"""
|
||||||
@ -704,6 +668,17 @@ class PlaylistTab(QTableView):
|
|||||||
else:
|
else:
|
||||||
self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH)
|
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:
|
def _span_cells(self, row: int, column: int, rowSpan: int, columnSpan: int) -> None:
|
||||||
"""
|
"""
|
||||||
Implement spanning of cells, initiated by signal
|
Implement spanning of cells, initiated by signal
|
||||||
@ -711,9 +686,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
model = self.proxy_model
|
model = self.proxy_model
|
||||||
if hasattr(model, "mapToSource"):
|
if hasattr(model, "mapToSource"):
|
||||||
edit_index = model.mapFromSource(
|
edit_index = model.mapFromSource(self.data_model.createIndex(row, column))
|
||||||
self.playlist_model.createIndex(row, column)
|
|
||||||
)
|
|
||||||
row = edit_index.row()
|
row = edit_index.row()
|
||||||
column = edit_index.column()
|
column = edit_index.column()
|
||||||
|
|
||||||
@ -731,5 +704,5 @@ class PlaylistTab(QTableView):
|
|||||||
def _unmark_as_next(self) -> None:
|
def _unmark_as_next(self) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
self.playlist_model.set_next_row(None)
|
self.data_model.set_next_row(None)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# #!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
#
|
#
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ def test_get_relative_date():
|
|||||||
assert get_relative_date(None) == "Never"
|
assert get_relative_date(None) == "Never"
|
||||||
today_at_10 = datetime.now().replace(hour=10, minute=0)
|
today_at_10 = datetime.now().replace(hour=10, minute=0)
|
||||||
today_at_11 = datetime.now().replace(hour=11, minute=0)
|
today_at_11 = datetime.now().replace(hour=11, minute=0)
|
||||||
assert get_relative_date(today_at_10, today_at_11) == "10:00"
|
assert get_relative_date(today_at_10, today_at_11) == "Today 10:00"
|
||||||
eight_days_ago = today_at_10 - timedelta(days=8)
|
eight_days_ago = today_at_10 - timedelta(days=8)
|
||||||
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
||||||
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user