Compare commits

...

8 Commits

Author SHA1 Message Date
Keith Edmunds
ab084ccf97 Fixup tests for section timings 2023-11-08 23:22:32 +00:00
Keith Edmunds
b399abb471 WIP V3: section timings in place 2023-11-08 23:18:33 +00:00
Keith Edmunds
6d648a56b7 WIP V3: fix editing headers rows 2023-11-08 18:34:10 +00:00
Keith Edmunds
b3262b2ede WIP V3: track start/end times working 2023-11-08 18:15:57 +00:00
Keith Edmunds
698fa4625a WIP V3: track start/stop times basics working
Only updates from header rows or current track. Changing
current track doesn't update correctly.
2023-11-07 23:14:26 +00:00
Keith Edmunds
b042ea10ec Move test_playlists.py to X_test_playlists for now 2023-11-07 20:50:39 +00:00
Keith Edmunds
9b682564ee WIP V3: remove redundant test.py 2023-11-07 20:42:34 +00:00
Keith Edmunds
813588e8e9 WIP V3: track stop implemented 2023-11-07 20:11:12 +00:00
8 changed files with 380 additions and 175 deletions

View File

@ -1,4 +1,4 @@
from PyQt5.QtCore import Qt from PyQt6.QtCore import Qt
from app import playlists from app import playlists
from app import models from app import models

View File

@ -81,7 +81,7 @@ class Config(object):
MAX_MISSING_FILES_TO_REPORT = 10 MAX_MISSING_FILES_TO_REPORT = 10
MILLISECOND_SIGFIGS = 0 MILLISECOND_SIGFIGS = 0
MINIMUM_ROW_HEIGHT = 30 MINIMUM_ROW_HEIGHT = 30
NOTE_TIME_FORMAT = "%H:%M:%S" NOTE_TIME_FORMAT = "%H:%M"
OBS_HOST = "localhost" OBS_HOST = "localhost"
OBS_PASSWORD = "auster" OBS_PASSWORD = "auster"
OBS_PORT = 4455 OBS_PORT = 4455

View File

@ -4,6 +4,7 @@ from typing import Any, Dict, Optional
import functools import functools
import os import os
import psutil import psutil
import re
import shutil import shutil
import smtplib import smtplib
import ssl import ssl
@ -19,6 +20,8 @@ from tinytag import TinyTag # type: ignore
from config import Config from config import Config
from log import log from log import log
start_time_re = re.compile(r"@\d\d:\d\d")
# Classes are defined after global functions so that classes can use # Classes are defined after global functions so that classes can use
# those functions. # those functions.
@ -95,20 +98,47 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]:
return None return None
def get_tags(path: str) -> Dict[str, Any]: def get_embedded_time(text: str) -> Optional[datetime]:
""" """Return datetime specified as @hh:mm:ss in text"""
Return a dictionary of title, artist, duration-in-milliseconds and path.
"""
tag = TinyTag.get(path) try:
match = start_time_re.search(text)
except TypeError:
return None
if not match:
return None
return dict( try:
title=tag.title, return datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT)
artist=tag.artist, except ValueError:
bitrate=round(tag.bitrate), return None
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
path=path,
) def get_file_metadata(filepath: str) -> dict:
"""Return track metadata"""
# Get title, artist, bitrate, duration, path
metadata: Dict[str, str | int | float] = get_tags(filepath)
metadata["mtime"] = os.path.getmtime(filepath)
# Set start_gap, fade_at and silence_at
audio = get_audio_segment(filepath)
if not audio:
audio_values = dict(start_gap=0, fade_at=0, silence_at=0)
else:
audio_values = dict(
start_gap=leading_silence(audio),
fade_at=int(
round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
),
silence_at=int(
round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
),
)
metadata |= audio_values
return metadata
def get_relative_date( def get_relative_date(
@ -153,31 +183,20 @@ def get_relative_date(
return f"{weeks} {weeks_str}, {days} {days_str} ago" return f"{weeks} {weeks_str}, {days} {days_str} ago"
def get_file_metadata(filepath: str) -> dict: def get_tags(path: str) -> Dict[str, Any]:
"""Return track metadata""" """
Return a dictionary of title, artist, duration-in-milliseconds and path.
"""
# Get title, artist, bitrate, duration, path tag = TinyTag.get(path)
metadata: Dict[str, str | int | float] = get_tags(filepath)
metadata["mtime"] = os.path.getmtime(filepath) return dict(
title=tag.title,
# Set start_gap, fade_at and silence_at artist=tag.artist,
audio = get_audio_segment(filepath) bitrate=round(tag.bitrate),
if not audio: duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
audio_values = dict(start_gap=0, fade_at=0, silence_at=0) path=path,
else: )
audio_values = dict(
start_gap=leading_silence(audio),
fade_at=int(
round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
),
silence_at=int(
round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
),
)
metadata |= audio_values
return metadata
def leading_silence( def leading_silence(

View File

@ -684,51 +684,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionPlay_next.setEnabled(True) self.actionPlay_next.setEnabled(True)
self.statusbar.showMessage("Play controls: Enabled", 0) self.statusbar.showMessage("Play controls: Enabled", 0)
def end_of_track_actions(self) -> None:
"""
Clean up after track played
Actions required:
- Set flag to say we're not playing a track
- Tell playlist_tab track has finished
- Reset PlaylistTrack objects
- Reset clocks
- Reset fade graph
- Update headers
- Enable controls
"""
# Set flag to say we're not playing a track so that timer ticks
# don't see player=None and kick off end-of-track actions
self.playing = False
# Tell playlist_tab track has finished
# TODO Reimplement as a signal
# if self.current_track.playlist_tab:
# self.current_track.playlist_tab.play_ended()
# Reset fade graph
if track_sequence.now.fade_graph:
track_sequence.now.fade_graph.clear()
# Reset PlaylistTrack objects
if track_sequence.now.track_id:
track_sequence.previous = track_sequence.now
track_sequence.now = PlaylistTrack()
# Reset clocks
self.frame_fade.setStyleSheet("")
self.frame_silent.setStyleSheet("")
self.label_elapsed_timer.setText("00:00 / 00:00")
self.label_fade_timer.setText("00:00")
self.label_silent_timer.setText("00:00")
# Update headers
self.update_headers()
# Enable controls
self.enable_play_next_controls()
def export_playlist_tab(self) -> None: def export_playlist_tab(self) -> None:
"""Export the current playlist to an m3u file""" """Export the current playlist to an m3u file"""
@ -1474,19 +1429,29 @@ class Window(QMainWindow, Ui_MainWindow):
self.stop_playing(fade=False) self.stop_playing(fade=False)
def stop_playing(self, fade=True) -> None: def stop_playing(self, fade: bool = True) -> None:
""" """
Stop playing current track Stop playing current track
Actions required: Actions required:
- Set flag to say we're not playing a track
- Return if not playing - Return if not playing
- Stop/fade track - Stop/fade track
- Reset playlist_tab colour - Reset playlist_tab colour
- Run end-of-track actions - Tell playlist_tab track has finished
- Reset PlaylistTrack objects
- Reset clocks
- Reset fade graph
- Update headers
- Enable controls
""" """
# Return if not playing # Set flag to say we're not playing a track so that timer ticks
if not self.playing: # don't see player=None and kick off end-of-track actions
if self.playing:
self.playing = False
else:
# Return if not playing
return return
# Stop/fade track # Stop/fade track
@ -1496,20 +1461,30 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
self.music.stop() self.music.stop()
# Reset playlist_tab colour # Reset fade graph
# TODO Reimplement if track_sequence.now.fade_graph:
# if self.current_track.playlist_tab: track_sequence.now.fade_graph.clear()
# if self.current_track.playlist_tab == self.next_track.playlist_tab:
# self.set_tab_colour(
# self.current_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
# )
# else:
# self.set_tab_colour(
# self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
# )
# Run end-of-track actions # Reset track_sequence objects
self.end_of_track_actions() if track_sequence.now.track_id:
track_sequence.previous = track_sequence.now
track_sequence.now = PlaylistTrack()
# Tell model previous track has finished
self.active_model().previous_track_ended()
# Reset clocks
self.frame_fade.setStyleSheet("")
self.frame_silent.setStyleSheet("")
self.label_elapsed_timer.setText("00:00 / 00:00")
self.label_fade_timer.setText("00:00")
self.label_silent_timer.setText("00:00")
# Update headers
self.update_headers()
# Enable controls
self.enable_play_next_controls()
def tab_change(self): def tab_change(self):
"""Called when active tab changed""" """Called when active tab changed"""

View File

@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timedelta
from enum import auto, Enum from enum import auto, Enum
from typing import List, Optional from typing import List, Optional
@ -17,7 +17,7 @@ from PyQt6.QtGui import (
from classes import track_sequence, MusicMusterSignals, PlaylistTrack from classes import track_sequence, MusicMusterSignals, PlaylistTrack
from config import Config from config import Config
from dbconfig import scoped_session, Session from dbconfig import scoped_session, Session
from helpers import file_is_unreadable from helpers import file_is_unreadable, get_embedded_time, ms_to_mmss
from log import log from log import log
from models import Playdates, PlaylistRows, Tracks from models import Playdates, PlaylistRows, Tracks
@ -43,14 +43,16 @@ class PlaylistRowData:
Populate PlaylistRowData from database PlaylistRows record Populate PlaylistRowData from database PlaylistRows record
""" """
self.start_gap: Optional[int] = None
self.title: str = ""
self.artist: str = "" self.artist: str = ""
self.duration: int = 0
self.lastplayed: datetime = Config.EPOCH
self.bitrate = 0 self.bitrate = 0
self.duration: int = 0
self.end_time: Optional[datetime] = None
self.lastplayed: datetime = Config.EPOCH
self.path = "" self.path = ""
self.played = False self.played = False
self.start_gap: Optional[int] = None
self.start_time: Optional[datetime] = None
self.title: str = ""
self.plrid: int = plr.id self.plrid: int = plr.id
self.plr_rownum: int = plr.plr_rownum self.plr_rownum: int = plr.plr_rownum
@ -108,6 +110,7 @@ class PlaylistModel(QAbstractTableModel):
with Session() as session: with Session() as session:
self.refresh_data(session) self.refresh_data(session)
self.update_track_times()
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -232,31 +235,38 @@ class PlaylistModel(QAbstractTableModel):
- find next track - find next track
""" """
row_number = track_sequence.now.plr_rownum
# Sanity check # Sanity check
if not track_sequence.now.track_id: if not track_sequence.now.track_id:
log.error( log.error(
"playlistmodel:current_track_started called with no current track" "playlistmodel:current_track_started called with no current track"
) )
return return
if track_sequence.now.plr_rownum is None: if row_number is None:
log.error( log.error(
"playlistmodel:current_track_started called with no row number " "playlistmodel:current_track_started called with no row number "
f"({track_sequence.now=})" f"({track_sequence.now=})"
) )
return return
# Update display
self.invalidate_row(track_sequence.now.plr_rownum)
# Update track times
# TODO
# Update Playdates in database # Update Playdates in database
with Session() as session: with Session() as session:
Playdates(session, track_sequence.now.track_id) Playdates(session, track_sequence.now.track_id)
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
self.refresh_row(session, plr.plr_rownum)
# Update track times
self.playlist_rows[row_number].start_time = datetime.now()
self.playlist_rows[row_number].end_time = datetime.now() + timedelta(
milliseconds=self.playlist_rows[row_number].duration
)
# Update colour and times for current row
self.invalidate_row(row_number)
# Update all other track times
self.update_track_times()
# Find next track # Find next track
# Get all unplayed track rows # Get all unplayed track rows
@ -269,13 +279,7 @@ class PlaylistModel(QAbstractTableModel):
if unplayed_rows: if unplayed_rows:
try: try:
# Find next row after current track # Find next row after current track
next_row = min( next_row = min([a for a in unplayed_rows if a > row_number])
[
a
for a in unplayed_rows
if a > track_sequence.now.plr_rownum
]
)
except ValueError: except ValueError:
# Find first unplayed track # Find first unplayed track
next_row = min(unplayed_rows) next_row = min(unplayed_rows)
@ -335,7 +339,7 @@ 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(prd.note) return QVariant(self.header_text(prd))
else: else:
return QVariant() return QVariant()
@ -346,11 +350,17 @@ class PlaylistModel(QAbstractTableModel):
if column == Col.ARTIST.value: if column == Col.ARTIST.value:
return QVariant(prd.artist) return QVariant(prd.artist)
if column == Col.DURATION.value: if column == Col.DURATION.value:
return QVariant(prd.duration) return QVariant(ms_to_mmss(prd.duration))
if column == Col.START_TIME.value: if column == Col.START_TIME.value:
return QVariant("FIXME") if prd.start_time:
return QVariant(prd.start_time.strftime(Config.TRACK_TIME_FORMAT))
else:
return QVariant()
if column == Col.END_TIME.value: if column == Col.END_TIME.value:
return QVariant("FIXME") if prd.end_time:
return QVariant(prd.end_time.strftime(Config.TRACK_TIME_FORMAT))
else:
return QVariant()
if column == Col.LAST_PLAYED.value: if column == Col.LAST_PLAYED.value:
return QVariant(prd.lastplayed) return QVariant(prd.lastplayed)
if column == Col.BITRATE.value: if column == Col.BITRATE.value:
@ -449,6 +459,60 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
def header_text(self, prd: PlaylistRowData) -> str:
"""
Process possible section timing directives embeded in header
"""
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:
duration += row_prd.duration
return (
f"{prd.note[:-1].strip()} "
f"[{count} tracks, {ms_to_mmss(duration)} unplayed (to end of playlist)]"
)
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(("+", "=")):
stripped_note = prd.note[:-1].strip()
if stripped_note:
return (
f"{stripped_note} [{count} tracks, "
f"{ms_to_mmss(duration)} unplayed]"
)
else:
return (
f"[Subtotal: {count} tracks, "
f"{ms_to_mmss(duration)} unplayed]"
)
else:
continue
else:
count += 1
if not row_prd.played:
duration += row_prd.duration
return prd.note
def is_header_row(self, row_number: int) -> bool: def is_header_row(self, row_number: int) -> bool:
""" """
Return True if row is a header row, else False Return True if row is a header row, else False
@ -581,6 +645,33 @@ class PlaylistModel(QAbstractTableModel):
# Update display # Update display
self.invalidate_rows(list(row_map.keys())) self.invalidate_rows(list(row_map.keys()))
def previous_track_ended(self) -> None:
"""
Notification from musicmuster that the previous track has ended.
Actions required:
- sanity check
- update display
- update track times
"""
# Sanity check
if not track_sequence.previous.track_id:
log.error("playlistmodel:previous_track_ended called with no current track")
return
if track_sequence.previous.plr_rownum is None:
log.error(
"playlistmodel:previous_track_ended called with no row number "
f"({track_sequence.previous=})"
)
return
# Update display
self.invalidate_row(track_sequence.previous.plr_rownum)
# Update track times
# TODO
def refresh_data(self, session: scoped_session): def refresh_data(self, session: scoped_session):
"""Populate dicts for data calls""" """Populate dicts for data calls"""
@ -593,7 +684,7 @@ class PlaylistModel(QAbstractTableModel):
"""Populate dict for one row for data calls""" """Populate dict for one row for data calls"""
p = PlaylistRows.deep_row(session, self.playlist_id, row_number) p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
self.playlist_rows[p.plr_rownum] = PlaylistRowData(p) self.playlist_rows[row_number] = PlaylistRowData(p)
def rowCount(self, index: QModelIndex = QModelIndex()) -> int: def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
"""Standard function for view""" """Standard function for view"""
@ -619,7 +710,7 @@ class PlaylistModel(QAbstractTableModel):
plr = session.get(PlaylistRows, plrid) plr = session.get(PlaylistRows, plrid)
if plr: if plr:
# Check this isn't a header row # Check this isn't a header row
if plr.track is None: if self.is_header_row(row_number):
return return
# Check track is readable # Check track is readable
if file_is_unreadable(plr.track.path): if file_is_unreadable(plr.track.path):
@ -627,6 +718,7 @@ class PlaylistModel(QAbstractTableModel):
track_sequence.next.set_plr(session, plr) track_sequence.next.set_plr(session, plr)
self.signals.next_track_changed_signal.emit() self.signals.next_track_changed_signal.emit()
self.invalidate_row(row_number) self.invalidate_row(row_number)
self.update_track_times()
def setData( def setData(
self, index: QModelIndex, value: QVariant, role: int = Qt.ItemDataRole.EditRole self, index: QModelIndex, value: QVariant, role: int = Qt.ItemDataRole.EditRole
@ -648,20 +740,25 @@ class PlaylistModel(QAbstractTableModel):
) )
return False return False
if column == Col.TITLE.value or column == Col.ARTIST.value: if plr.track_id:
track = session.get(Tracks, plr.track_id) if column == Col.TITLE.value or column == Col.ARTIST.value:
if not track: track = session.get(Tracks, plr.track_id)
print(f"Error retreiving track: {plr=}") if not track:
return False print(f"Error retreiving track: {plr=}")
if column == Col.TITLE.value: return False
track.title = str(value) if column == Col.TITLE.value:
elif column == Col.ARTIST.value: track.title = str(value)
track.artist = str(value) elif column == Col.ARTIST.value:
else: track.artist = str(value)
print(f"Error updating track: {column=}, {value=}") else:
return False print(f"Error updating track: {column=}, {value=}")
elif column == Col.NOTE.value: return False
plr.note = str(value) elif column == Col.NOTE.value:
plr.note = str(value)
else:
# This is a header row
if column == HEADER_NOTES_COLUMN:
plr.note = str(value)
# Flush changes before refreshing data # Flush changes before refreshing data
session.flush() session.flush()
@ -671,5 +768,82 @@ class PlaylistModel(QAbstractTableModel):
return False return False
def supportedDropActions(self): def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
def update_track_times(self) -> None:
"""
Update track start/end times in self.playlist_rows
"""
next_start_time: Optional[datetime] = None
update_rows: List[int] = []
for row_number in range(len(self.playlist_rows)):
prd = self.playlist_rows[row_number]
# Reset start_time if this is the current row
if row_number == track_sequence.now.plr_rownum:
# Start/end times for current track are set in current_track_started
if not next_start_time:
next_start_time = prd.end_time
continue
# 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 current_end_time:
prd.start_time = current_end_time
prd.end_time = current_end_time + 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
# 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 (
track_sequence.now.plr_rownum is not None
and track_sequence.next.plr_rownum is not None
and track_sequence.now.plr_rownum
< row_number
< track_sequence.next.plr_rownum
):
prd.start_time = None
prd.end_time = None
update_rows.append(row_number)
continue
# Reset start time if timing in header or at current track
if not prd.path:
# This is a header row
header_time = get_embedded_time(prd.note)
if header_time:
next_start_time = header_time
else:
# This is an unplayed track; set start/end if we have a
# start time
if next_start_time is None:
continue
if prd.start_time != next_start_time:
prd.start_time = next_start_time
update_rows.append(row_number)
next_start_time += timedelta(
milliseconds=self.playlist_rows[row_number].duration
)
if prd.end_time != next_start_time:
prd.end_time = next_start_time
update_rows.append(row_number)
# Update start/stop times of rows that have changed
for updated_row in update_rows:
self.dataChanged.emit(
self.index(updated_row, Col.START_TIME.value),
self.index(updated_row, Col.END_TIME.value),
)

View File

@ -113,7 +113,7 @@ class EscapeDelegate(QStyledItemDelegate):
return True return True
elif key_event.key() == Qt.Key.Key_Escape: elif key_event.key() == Qt.Key.Key_Escape:
discard_edits = QMessageBox.question( discard_edits = QMessageBox.question(
cast(QWidget, self), "Abandon edit", "Discard changes?" cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
) )
if discard_edits == QMessageBox.StandardButton.Yes: if discard_edits == QMessageBox.StandardButton.Yes:
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
@ -1064,7 +1064,6 @@ class PlaylistTab(QTableView):
header_row = False header_row = False
model = cast(PlaylistModel, self.model()) model = cast(PlaylistModel, self.model())
if model: if model:
header_row = model.is_header_row(row_number) header_row = model.is_header_row(row_number)
# current = row_number == self._get_current_track_row_number() # current = row_number == self._get_current_track_row_number()
# next_row = row_number == self._get_next_track_row_number() # next_row = row_number == self._get_next_track_row_number()

33
test.py
View File

@ -1,33 +0,0 @@
#!/usr/bin/env python
from PyQt5 import QtGui, QtWidgets
class TabBar(QtWidgets.QTabBar):
def paintEvent(self, event):
painter = QtWidgets.QStylePainter(self)
option = QtWidgets.QStyleOptionTab()
for index in range(self.count()):
self.initStyleOption(option, index)
bgcolor = QtGui.QColor(self.tabText(index))
option.palette.setColor(QtGui.QPalette.Window, bgcolor)
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option)
class Window(QtWidgets.QTabWidget):
def __init__(self):
QtWidgets.QTabWidget.__init__(self)
self.setTabBar(TabBar(self))
for color in "tomato orange yellow lightgreen skyblue plum".split():
self.addTab(QtWidgets.QWidget(self), color)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.resize(420, 200)
window.show()
sys.exit(app.exec_())

View File

@ -1,9 +1,37 @@
from app.models import ( from app.models import (
Playlists, Playlists,
Tracks,
) )
from app.helpers import get_file_metadata
from app import playlistmodel from app import playlistmodel
from dbconfig import scoped_session from dbconfig import scoped_session
test_tracks = [
"testdata/isa.mp3",
"testdata/isa_with_gap.mp3",
"testdata/loser.mp3",
"testdata/lovecats-10seconds.mp3",
"testdata/lovecats.mp3",
"testdata/mom.mp3",
"testdata/sitting.mp3",
]
def create_model_with_tracks(session: scoped_session) -> "playlistmodel.PlaylistModel":
playlist = Playlists(session, "test playlist")
model = playlistmodel.PlaylistModel(playlist.id)
for row in range(len(test_tracks)):
track_path = test_tracks[row % len(test_tracks)]
metadata = get_file_metadata(track_path)
track = Tracks(session, **metadata)
model.insert_track_row(row, track.id, f"{row=}")
session.commit()
return model
def create_model_with_playlist_rows( def create_model_with_playlist_rows(
session: scoped_session, rows: int session: scoped_session, rows: int
) -> "playlistmodel.PlaylistModel": ) -> "playlistmodel.PlaylistModel":
@ -197,3 +225,46 @@ def test_insert_header_row_middle(monkeypatch, session):
model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd) model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd)
== note_text == note_text
) )
def test_create_model_with_tracks(monkeypatch, session):
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_tracks(session)
assert len(model.playlist_rows) == len(test_tracks)
def test_timing_one_track(monkeypatch, session):
START_ROW = 0
END_ROW = 2
monkeypatch.setattr(playlistmodel, "Session", session)
model = create_model_with_tracks(session)
model.insert_header_row(START_ROW, "start+")
model.insert_header_row(END_ROW, "-")
prd = model.playlist_rows[START_ROW]
qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd)
assert qv_value.value() == "start [1 tracks, 4:23 unplayed]"
# def test_edit_header(monkeypatch, session): # edit header row in middle of playlist
# monkeypatch.setattr(playlistmodel, "Session", session)
# note_text = "test text"
# initial_row_count = 11
# insert_row = 6
# model = create_model_with_playlist_rows(session, initial_row_count)
# model.insert_header_row(insert_row, note_text)
# assert model.rowCount() == initial_row_count + 1
# prd = model.playlist_rows[insert_row]
# # Test against edit_role because display_role for headers is
# # handled differently (sets up row span)
# assert (
# model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd)
# == note_text
# )