Compare commits
5 Commits
d5871fe77f
...
de710b1dc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de710b1dc7 | ||
|
|
3cbc69b11e | ||
|
|
56087870f4 | ||
|
|
b83bd0d5c3 | ||
|
|
3e49ad08b9 |
@ -1,5 +1,7 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import auto, Enum
|
from enum import auto, Enum
|
||||||
|
from operator import attrgetter
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
@ -20,6 +22,7 @@ from dbconfig import scoped_session, Session
|
|||||||
from helpers import (
|
from helpers import (
|
||||||
file_is_unreadable,
|
file_is_unreadable,
|
||||||
get_embedded_time,
|
get_embedded_time,
|
||||||
|
get_relative_date,
|
||||||
open_in_audacity,
|
open_in_audacity,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
set_track_metadata,
|
set_track_metadata,
|
||||||
@ -52,12 +55,10 @@ class PlaylistRowData:
|
|||||||
self.artist: str = ""
|
self.artist: str = ""
|
||||||
self.bitrate = 0
|
self.bitrate = 0
|
||||||
self.duration: int = 0
|
self.duration: int = 0
|
||||||
self.end_time: Optional[datetime] = None
|
|
||||||
self.lastplayed: datetime = Config.EPOCH
|
self.lastplayed: datetime = Config.EPOCH
|
||||||
self.path = ""
|
self.path = ""
|
||||||
self.played = False
|
self.played = False
|
||||||
self.start_gap: Optional[int] = None
|
self.start_gap: Optional[int] = None
|
||||||
self.start_time: Optional[datetime] = None
|
|
||||||
self.title: str = ""
|
self.title: str = ""
|
||||||
|
|
||||||
self.plrid: int = plr.id
|
self.plrid: int = plr.id
|
||||||
@ -84,6 +85,12 @@ class PlaylistRowData:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StartEndTimes:
|
||||||
|
start_time: Optional[datetime] = None
|
||||||
|
end_time: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class PlaylistModel(QAbstractTableModel):
|
class PlaylistModel(QAbstractTableModel):
|
||||||
"""
|
"""
|
||||||
The Playlist Model
|
The Playlist Model
|
||||||
@ -110,6 +117,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.playlist_rows: dict[int, PlaylistRowData] = {}
|
self.playlist_rows: dict[int, PlaylistRowData] = {}
|
||||||
|
self.start_end_times: dict[int, StartEndTimes] = {}
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
|
|
||||||
self.signals.add_track_to_header_signal.connect(self.add_track_to_header)
|
self.signals.add_track_to_header_signal.connect(self.add_track_to_header)
|
||||||
@ -266,10 +274,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.refresh_row(session, plr.plr_rownum)
|
self.refresh_row(session, plr.plr_rownum)
|
||||||
|
|
||||||
# Update track times
|
# Update track times
|
||||||
self.playlist_rows[row_number].start_time = datetime.now()
|
self.start_end_times[row_number].start_time = track_sequence.now.start_time
|
||||||
self.playlist_rows[row_number].end_time = datetime.now() + timedelta(
|
self.start_end_times[row_number].end_time = track_sequence.now.end_time
|
||||||
milliseconds=self.playlist_rows[row_number].duration
|
|
||||||
)
|
|
||||||
# Update colour and times for current row
|
# Update colour and times for current row
|
||||||
self.invalidate_row(row_number)
|
self.invalidate_row(row_number)
|
||||||
# Update all other track times
|
# Update all other track times
|
||||||
@ -367,17 +374,19 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if column == Col.DURATION.value:
|
if column == Col.DURATION.value:
|
||||||
return QVariant(ms_to_mmss(prd.duration))
|
return QVariant(ms_to_mmss(prd.duration))
|
||||||
if column == Col.START_TIME.value:
|
if column == Col.START_TIME.value:
|
||||||
if prd.start_time:
|
if row in self.start_end_times:
|
||||||
return QVariant(prd.start_time.strftime(Config.TRACK_TIME_FORMAT))
|
start_time = self.start_end_times[row].start_time
|
||||||
else:
|
if start_time:
|
||||||
|
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 prd.end_time:
|
if row in self.start_end_times:
|
||||||
return QVariant(prd.end_time.strftime(Config.TRACK_TIME_FORMAT))
|
end_time = self.start_end_times[row].end_time
|
||||||
else:
|
if end_time:
|
||||||
|
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
|
||||||
return QVariant()
|
return QVariant()
|
||||||
if column == Col.LAST_PLAYED.value:
|
if column == Col.LAST_PLAYED.value:
|
||||||
return QVariant(prd.lastplayed)
|
return QVariant(get_relative_date(prd.lastplayed))
|
||||||
if column == Col.BITRATE.value:
|
if column == Col.BITRATE.value:
|
||||||
return QVariant(prd.bitrate)
|
return QVariant(prd.bitrate)
|
||||||
if column == Col.NOTE.value:
|
if column == Col.NOTE.value:
|
||||||
@ -647,13 +656,20 @@ 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 mark_unplayed(self, row_number: int) -> None:
|
def mark_unplayed(self, row_numbers: List[int]) -> None:
|
||||||
"""
|
"""
|
||||||
Mark row as unplayed
|
Mark row as unplayed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.playlist_rows[row_number].played = False
|
with Session() as session:
|
||||||
self.invalidate_row(row_number)
|
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:
|
||||||
"""
|
"""
|
||||||
@ -676,6 +692,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
from_rows, range(next_to_row, next_to_row + len(from_rows))
|
from_rows, range(next_to_row, next_to_row + len(from_rows))
|
||||||
):
|
):
|
||||||
row_map[from_row] = to_row
|
row_map[from_row] = to_row
|
||||||
|
|
||||||
# Move the remaining rows to the row_map. We want to fill it
|
# Move the remaining rows to the row_map. We want to fill it
|
||||||
# before (if there are gaps) and after (likewise) the rows that
|
# before (if there are gaps) and after (likewise) the rows that
|
||||||
# are moving.
|
# are moving.
|
||||||
@ -693,6 +710,14 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
old_row, HEADER_NOTES_COLUMN, 1, 1
|
old_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[track_sequence.previous.plr_rownum]
|
||||||
|
if track_sequence.now.plr_rownum in row_map:
|
||||||
|
track_sequence.now.plr_rownum = row_map[track_sequence.now.plr_rownum]
|
||||||
|
if track_sequence.next.plr_rownum in row_map:
|
||||||
|
track_sequence.next.plr_rownum = row_map[track_sequence.next.plr_rownum]
|
||||||
|
|
||||||
# For SQLAlchemy, build a list of dictionaries that map plrid to
|
# For SQLAlchemy, build a list of dictionaries that map plrid to
|
||||||
# new row number:
|
# new row number:
|
||||||
sqla_map: List[dict[str, int]] = []
|
sqla_map: List[dict[str, int]] = []
|
||||||
@ -706,6 +731,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
|
|
||||||
# Update display
|
# Update display
|
||||||
|
self.update_track_times()
|
||||||
self.invalidate_rows(list(row_map.keys()))
|
self.invalidate_rows(list(row_map.keys()))
|
||||||
|
|
||||||
def open_in_audacity(self, row_number: int) -> None:
|
def open_in_audacity(self, row_number: int) -> None:
|
||||||
@ -753,7 +779,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.playlist_rows[p.plr_rownum] = PlaylistRowData(p)
|
self.playlist_rows[p.plr_rownum] = PlaylistRowData(p)
|
||||||
|
|
||||||
def refresh_row(self, session, row_number):
|
def refresh_row(self, session, row_number):
|
||||||
"""Populate dict for one row for data calls"""
|
"""Populate dict for one row from database"""
|
||||||
|
|
||||||
p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
|
p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
|
||||||
self.playlist_rows[row_number] = PlaylistRowData(p)
|
self.playlist_rows[row_number] = PlaylistRowData(p)
|
||||||
@ -897,35 +923,44 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Sort selected rows by artist
|
Sort selected rows by artist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
self.sort_by_attribute(row_numbers, 'artist')
|
||||||
|
|
||||||
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
def sort_by_attribute(self, row_numbers: List[int], attr_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sort selected rows by duration
|
Sort selected rows by passed attribute name where 'attribute' is a
|
||||||
"""
|
key in PlaylistRowData
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
|
||||||
"""
|
|
||||||
Sort selected rows by lastplayed
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
def sort_by_title(self, row_numbers: List[int]) -> None:
|
|
||||||
"""
|
|
||||||
Sort selected rows by title
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create a subset of playlist_rows with the rows we are
|
# Create a subset of playlist_rows with the rows we are
|
||||||
# interested in
|
# interested in
|
||||||
shortlist_rows = {k: self.playlist_rows[k] for k in row_numbers}
|
shortlist_rows = {k: self.playlist_rows[k] for k in row_numbers}
|
||||||
sorted_list = [
|
sorted_list = [
|
||||||
k for k, v in sorted(shortlist_rows.items(), key=lambda item: item[1].title)
|
plr.plr_rownum for plr in
|
||||||
|
sorted(shortlist_rows.values(), key=attrgetter(attr_name))
|
||||||
]
|
]
|
||||||
self.move_rows(sorted_list, min(sorted_list))
|
self.move_rows(sorted_list, min(sorted_list))
|
||||||
|
|
||||||
|
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Sort selected rows by duration
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.sort_by_attribute(row_numbers, 'duration')
|
||||||
|
|
||||||
|
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Sort selected rows by lastplayed
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.sort_by_attribute(row_numbers, 'lastplayed')
|
||||||
|
|
||||||
|
def sort_by_title(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Sort selected rows by title
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.sort_by_attribute(row_numbers, 'title')
|
||||||
|
|
||||||
def supportedDropActions(self) -> Qt.DropAction:
|
def supportedDropActions(self) -> Qt.DropAction:
|
||||||
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
|
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
|
||||||
|
|
||||||
@ -938,21 +973,22 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
update_rows: List[int] = []
|
update_rows: List[int] = []
|
||||||
|
|
||||||
for row_number in range(len(self.playlist_rows)):
|
for row_number in range(len(self.playlist_rows)):
|
||||||
|
stend = self.start_end_times[row_number] = StartEndTimes()
|
||||||
prd = self.playlist_rows[row_number]
|
prd = self.playlist_rows[row_number]
|
||||||
|
|
||||||
# Reset start_time if this is the current row
|
# Reset start_time if this is the current row
|
||||||
if row_number == track_sequence.now.plr_rownum:
|
if row_number == track_sequence.now.plr_rownum:
|
||||||
# Start/end times for current track are set in current_track_started
|
stend.start_time = track_sequence.now.start_time
|
||||||
|
stend.end_time = track_sequence.now.end_time
|
||||||
if not next_start_time:
|
if not next_start_time:
|
||||||
next_start_time = prd.end_time
|
next_start_time = stend.end_time
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Set start time for next row if we have a current track
|
# Set start time for next row if we have a current track
|
||||||
current_end_time = track_sequence.now.end_time
|
if row_number == track_sequence.next.plr_rownum and track_sequence.now.end_time:
|
||||||
if row_number == track_sequence.next.plr_rownum and current_end_time:
|
stend.start_time = track_sequence.now.end_time
|
||||||
prd.start_time = current_end_time
|
stend.end_time = stend.start_time + timedelta(milliseconds=prd.duration)
|
||||||
prd.end_time = current_end_time + timedelta(milliseconds=prd.duration)
|
next_start_time = stend.end_time
|
||||||
next_start_time = prd.end_time
|
|
||||||
update_rows.append(row_number)
|
update_rows.append(row_number)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -973,14 +1009,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
< row_number
|
< row_number
|
||||||
< track_sequence.next.plr_rownum
|
< track_sequence.next.plr_rownum
|
||||||
):
|
):
|
||||||
prd.start_time = None
|
|
||||||
prd.end_time = None
|
|
||||||
update_rows.append(row_number)
|
update_rows.append(row_number)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Reset start time if timing in header or at current track
|
# Reset start time if timing in header or at current track
|
||||||
if not prd.path:
|
if self.is_header_row(row_number):
|
||||||
# This is a header row
|
|
||||||
header_time = get_embedded_time(prd.note)
|
header_time = get_embedded_time(prd.note)
|
||||||
if header_time:
|
if header_time:
|
||||||
next_start_time = header_time
|
next_start_time = header_time
|
||||||
@ -989,14 +1022,14 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# start time
|
# start time
|
||||||
if next_start_time is None:
|
if next_start_time is None:
|
||||||
continue
|
continue
|
||||||
if prd.start_time != next_start_time:
|
if stend.start_time != next_start_time:
|
||||||
prd.start_time = next_start_time
|
stend.start_time = next_start_time
|
||||||
update_rows.append(row_number)
|
update_rows.append(row_number)
|
||||||
next_start_time += timedelta(
|
next_start_time += timedelta(
|
||||||
milliseconds=self.playlist_rows[row_number].duration
|
milliseconds=self.playlist_rows[row_number].duration
|
||||||
)
|
)
|
||||||
if prd.end_time != next_start_time:
|
if stend.end_time != next_start_time:
|
||||||
prd.end_time = next_start_time
|
stend.end_time = next_start_time
|
||||||
update_rows.append(row_number)
|
update_rows.append(row_number)
|
||||||
|
|
||||||
# Update start/stop times of rows that have changed
|
# Update start/stop times of rows that have changed
|
||||||
|
|||||||
@ -1103,11 +1103,15 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Mark unplayed
|
# Mark unplayed
|
||||||
if track_row and model.is_unplayed_row(row_number):
|
if track_row and model.is_unplayed_row(row_number):
|
||||||
self._add_context_menu("Mark unplayed", lambda: model.mark_unplayed(row_number))
|
self._add_context_menu(
|
||||||
|
"Mark unplayed", lambda: model.mark_unplayed(self._get_selected_rows())
|
||||||
|
)
|
||||||
|
|
||||||
# Unmark as next
|
# Unmark as next
|
||||||
if next_row:
|
if next_row:
|
||||||
self._add_context_menu("Unmark as next track", lambda: model.set_next_row(None))
|
self._add_context_menu(
|
||||||
|
"Unmark as next track", lambda: model.set_next_row(None)
|
||||||
|
)
|
||||||
|
|
||||||
# ----------------------
|
# ----------------------
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
@ -1134,9 +1138,6 @@ class PlaylistTab(QTableView):
|
|||||||
lambda: model.sort_by_lastplayed(self._get_selected_rows()),
|
lambda: model.sort_by_lastplayed(self._get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
if sort_menu:
|
|
||||||
sort_menu.setEnabled(model.selection_is_sortable(self._get_selected_rows()))
|
|
||||||
self._add_context_menu("Undo sort", self._sort_undo, not bool(self.sort_undo))
|
|
||||||
|
|
||||||
# Info TODO
|
# Info TODO
|
||||||
if track_row:
|
if track_row:
|
||||||
@ -1144,8 +1145,7 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Track path TODO
|
# Track path TODO
|
||||||
if track_row:
|
if track_row:
|
||||||
self._add_context_menu(
|
self._add_context_menu("Copy track path", lambda: print("Track path"))
|
||||||
"Copy track path", lambda: print("Track path"))
|
|
||||||
|
|
||||||
def _calculate_end_time(
|
def _calculate_end_time(
|
||||||
self, start: Optional[datetime], duration: int
|
self, start: Optional[datetime], duration: int
|
||||||
|
|||||||
@ -202,7 +202,7 @@ def test_insert_header_row_end(monkeypatch, session):
|
|||||||
# Test against edit_role because display_role for headers is
|
# Test against edit_role because display_role for headers is
|
||||||
# handled differently (sets up row span)
|
# handled differently (sets up row span)
|
||||||
assert (
|
assert (
|
||||||
model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd)
|
model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd)
|
||||||
== note_text
|
== note_text
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ def test_insert_header_row_middle(monkeypatch, session):
|
|||||||
# Test against edit_role because display_role for headers is
|
# Test against edit_role because display_role for headers is
|
||||||
# handled differently (sets up row span)
|
# handled differently (sets up row span)
|
||||||
assert (
|
assert (
|
||||||
model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd)
|
model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd)
|
||||||
== note_text
|
== note_text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user