Refactor set track times

This commit is contained in:
Keith Edmunds 2024-07-22 16:29:17 +01:00
parent d7a37151b7
commit 3c884e54ca

View File

@ -94,6 +94,39 @@ class _PlaylistRowData:
f"note='{self.note}', title='{self.title}', artist='{self.artist}'>"
)
def set_start(
self, modified_rows: list[int], start: Optional[dt.datetime]
) -> Optional[dt.datetime]:
"""
Set start time for this row
Update passed modified rows list if we changed the row.
Return new start time
"""
changed = False
if self.start_time != start:
self.start_time = start
changed = True
if start is None:
if self.end_time is not None:
self.end_time = None
changed = True
new_start_time = None
else:
end_time = start + dt.timedelta(milliseconds=self.duration)
new_start_time = end_time
if self.end_time != end_time:
self.end_time = end_time
changed = True
if changed and self.plr_rownum not in modified_rows:
modified_rows.append(self.plr_rownum)
return new_start_time
class PlaylistModel(QAbstractTableModel):
"""
@ -310,7 +343,9 @@ class PlaylistModel(QAbstractTableModel):
session.commit()
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> QVariant:
def data(
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
) -> QVariant:
"""Return data to view"""
if not index.isValid() or not (0 <= index.row() < len(self.playlist_rows)):
@ -640,84 +675,15 @@ class PlaylistModel(QAbstractTableModel):
Process possible section timing directives embeded in header
"""
count: int = 0
unplayed_count: int = 0
duration: int = 0
if prd.note.endswith("+"):
# This header is the start of a timed section
for row_number in range(prd.plr_rownum + 1, len(self.playlist_rows)):
row_prd = self.playlist_rows[row_number]
if self.is_header_row(row_number):
if row_prd.note.endswith("-"):
return (
f"{prd.note[:-1].strip()} "
f"[{count} tracks, {ms_to_mmss(duration)} unplayed]"
)
else:
continue
else:
count += 1
if not row_prd.played:
unplayed_count += 1
duration += row_prd.duration
return (
f"{prd.note[:-1].strip()} "
f"[{count} tracks, {ms_to_mmss(duration, none='none')} "
"unplayed (to end of playlist)]"
)
return self.start_of_timed_section_header(prd)
elif prd.note.endswith("="):
# Show subtotal
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("-"):
# There was no start of section
return prd.note
if row_prd.note.endswith(("+", "=")):
# If we are playing this section, also
# calculate end time if all tracks are played.
end_time_str = ""
if (
track_sequence.current
and track_sequence.current.end_time
and (
row_number
< track_sequence.current.row_number
< prd.plr_rownum
)
):
section_end_time = (
track_sequence.current.end_time
+ dt.timedelta(milliseconds=duration)
)
end_time_str = (
", section end time "
+ section_end_time.strftime(Config.TRACK_TIME_FORMAT)
)
stripped_note = prd.note[:-1].strip()
if stripped_note:
return (
f"{stripped_note} ["
f"{unplayed_count}/{count} track{'s' if count > 1 else ''} "
f"({ms_to_mmss(duration)}) unplayed{end_time_str}]"
)
else:
return (
f"[{unplayed_count}/{count} track{'s' if count > 1 else ''} "
f"({ms_to_mmss(duration)}) unplayed{end_time_str}]"
)
else:
continue
else:
count += 1
if not row_prd.played:
unplayed_count += 1
duration += row_prd.duration
return self.section_subtotal_header(prd)
elif prd.note == "-":
# If the hyphen is the only thing on the line, echo the note
# tha started the section without the trailing "+".
# that 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):
@ -909,6 +875,7 @@ class PlaylistModel(QAbstractTableModel):
# Update display
self.reset_track_sequence_row_numbers()
self.update_track_times()
self.invalidate_rows(list(row_map.keys()))
def move_rows_between_playlists(
@ -1185,7 +1152,68 @@ class PlaylistModel(QAbstractTableModel):
return len(self.playlist_rows)
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
def section_subtotal_header(self, prd: _PlaylistRowData) -> str:
"""
Process this row as subtotal within a timed section and
return display text for this row
"""
count: int = 0
unplayed_count: int = 0
duration: int = 0
# Show subtotal
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("-"):
# There was no start of section
return prd.note
if row_prd.note.endswith(("+", "=")):
# If we are playing this section, also
# calculate end time if all tracks are played.
end_time_str = ""
if (
track_sequence.current
and track_sequence.current.end_time
and (
row_number
< track_sequence.current.row_number
< prd.plr_rownum
)
):
section_end_time = (
track_sequence.current.end_time
+ dt.timedelta(milliseconds=duration)
)
end_time_str = (
", section end time "
+ section_end_time.strftime(Config.TRACK_TIME_FORMAT)
)
stripped_note = prd.note[:-1].strip()
if stripped_note:
return (
f"{stripped_note} ["
f"{unplayed_count}/{count} track{'s' if count > 1 else ''} "
f"({ms_to_mmss(duration)}) unplayed{end_time_str}]"
)
else:
return (
f"[{unplayed_count}/{count} track{'s' if count > 1 else ''} "
f"({ms_to_mmss(duration)}) unplayed{end_time_str}]"
)
else:
continue
else:
count += 1
if not row_prd.played:
unplayed_count += 1
duration += row_prd.duration
# Should never get here
return f"Error calculating subtotal ({row_prd.note})"
def selection_is_sortable(self, row_numbers: list[int]) -> bool:
"""
Return True if the selection is sortable. That means:
- at least two rows selected
@ -1376,6 +1404,37 @@ class PlaylistModel(QAbstractTableModel):
self.sort_by_attribute(row_numbers, "title")
def start_of_timed_section_header(self, prd: _PlaylistRowData) -> str:
"""
Process this row as the start of a timed section and
return display text for this row
"""
count: int = 0
unplayed_count: int = 0
duration: int = 0
for row_number in range(prd.plr_rownum + 1, len(self.playlist_rows)):
row_prd = self.playlist_rows[row_number]
if self.is_header_row(row_number):
if row_prd.note.endswith("-"):
return (
f"{prd.note[:-1].strip()} "
f"[{count} tracks, {ms_to_mmss(duration)} unplayed]"
)
else:
continue
else:
count += 1
if not row_prd.played:
unplayed_count += 1
duration += row_prd.duration
return (
f"{prd.note[:-1].strip()} "
f"[{count} tracks, {ms_to_mmss(duration, none='none')} "
"unplayed (to end of playlist)]"
)
def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
@ -1408,51 +1467,36 @@ class PlaylistModel(QAbstractTableModel):
log.debug("update_track_times()")
next_start_time: Optional[dt.datetime] = None
update_rows: List[int] = []
playlist_length = len(self.playlist_rows)
if not playlist_length:
return
update_rows: list[int] = []
row_count = len(self.playlist_rows)
for row_number in range(playlist_length):
current_track_row = None
next_track_row = None
if track_sequence.current:
current_track_row = track_sequence.current.row_number
# Update current track details now so that they are available
# when we deal with next track row which may be above current
# track row.
self.playlist_rows[
current_track_row
].start_time = track_sequence.current.start_time
self.playlist_rows[
current_track_row
].end_time = track_sequence.current.end_time
update_rows.append(current_track_row)
if track_sequence.next:
next_track_row = track_sequence.next.row_number
for row_number in range(row_count):
prd = self.playlist_rows[row_number]
# Reset start_time if this is the current row
if track_sequence.current:
if row_number == track_sequence.current.row_number:
prd.start_time = track_sequence.current.start_time
prd.end_time = track_sequence.current.end_time
update_rows.append(row_number)
if not next_start_time:
next_start_time = prd.end_time
continue
# Set start time for next row if we have a current track
if track_sequence.next and track_sequence.current.end_time:
if row_number == track_sequence.next.row_number:
prd.start_time = track_sequence.current.end_time
prd.end_time = prd.start_time + dt.timedelta(
milliseconds=prd.duration
)
next_start_time = prd.end_time
update_rows.append(row_number)
continue
# Don't update times for tracks that have been played
if prd.played:
continue
# If we're between the current and next row, zero out
# times
# Don't update times for tracks that have been played, for
# unreadable tracks or for the current track, handled above.
if (
track_sequence.current
and track_sequence.next
and track_sequence.current.row_number
< row_number
< track_sequence.next.row_number
prd.played
or (prd.path and file_is_unreadable(prd.path))
or (current_track_row and row_number == current_track_row)
):
prd.start_time = None
prd.end_time = None
update_rows.append(row_number)
continue
# Reset start time if timing in header
@ -1462,28 +1506,24 @@ class PlaylistModel(QAbstractTableModel):
next_start_time = header_time
continue
# This is an unplayed track
# Don't schedule unplayable tracks
if file_is_unreadable(prd.path):
# Set start time for next row if we have a current track
if (
next_track_row
and row_number == next_track_row
and track_sequence.current
and track_sequence.current.end_time
):
next_start_time = prd.set_start(update_rows, track_sequence.current.end_time)
continue
# Set start/end if we have a start time
if next_start_time is None:
# If we're between the current and next row, zero out
# times
if (current_track_row or row_count) < row_number < (next_track_row or 0):
prd.set_start(update_rows, None)
continue
# Update start time of this row if it's incorrect
if prd.start_time != next_start_time:
prd.start_time = next_start_time
update_rows.append(row_number)
# Calculate next start time
next_start_time += dt.timedelta(milliseconds=prd.duration)
# Update end time of this row if it's incorrect
if prd.end_time != next_start_time:
prd.end_time = next_start_time
if row_number not in update_rows:
update_rows.append(row_number)
# Set start/end
next_start_time = prd.set_start(update_rows, next_start_time)
# Update start/stop times of rows that have changed
for updated_row in update_rows:
@ -1524,7 +1564,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# Don't hide current track
if (
track_sequence.current
and track_sequence.current.playlist_id == self.source_model.playlist_id
and track_sequence.current.playlist_id
== self.source_model.playlist_id
and track_sequence.current.row_number == source_row
):
return True
@ -1540,7 +1581,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# Handle previous track
if track_sequence.previous:
if (
track_sequence.previous.playlist_id != self.source_model.playlist_id
track_sequence.previous.playlist_id
!= self.source_model.playlist_id
or track_sequence.previous.row_number != source_row
):
# This row isn't our previous track: hide it
@ -1549,11 +1591,10 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# This row is our previous track. Don't hide it
# until HIDE_AFTER_PLAYING_OFFSET milliseconds
# after current track has started
if (
if track_sequence.current.start_time and dt.datetime.now() > (
track_sequence.current.start_time
and dt.datetime.now() > (
track_sequence.current.start_time
+ dt.timedelta(milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET)
+ dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
)
):
return False
@ -1565,9 +1606,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# true next time through.
QTimer.singleShot(
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
lambda: self.source_model.invalidate_row(
source_row
),
lambda: self.source_model.invalidate_row(source_row),
)
return True
# Next track not playing yet so don't hide previous