Rebase dev onto v2_id

This commit is contained in:
Keith Edmunds 2022-03-02 09:16:07 +00:00
parent b92a0927f8
commit cf58932fca
10 changed files with 461 additions and 239 deletions

View File

@ -3,6 +3,7 @@ import os
class Config(object):
AUDACITY_COMMAND = "/usr/bin/audacity"
AUDIO_SEGMENT_CHUNK_SIZE = 10
COLOUR_CURRENT_HEADER = "#d4edda"
COLOUR_CURRENT_PLAYLIST = "#7eca8f"
@ -19,8 +20,17 @@ class Config(object):
COLOUR_PREVIOUS_HEADER = "#f8d7da"
COLOUR_UNREADABLE = "#dc3545"
COLOUR_WARNING_TIMER = "#ffc107"
COLUMN_NAME_ARTIST = "Artist"
COLUMN_NAME_AUTOPLAY = "A"
COLUMN_NAME_END_TIME = "End"
COLUMN_NAME_LAST_PLAYED = "Last played"
COLUMN_NAME_LEADING_SILENCE = "Gap"
COLUMN_NAME_LENGTH = "Length"
COLUMN_NAME_START_TIME = "Start"
COLUMN_NAME_TITLE = "Title"
DBFS_FADE = -12
DBFS_SILENCE = -50
DEFAULT_COLUMN_WIDTH = 200
DEFAULT_IMPORT_DIRECTORY = "/home/kae/Nextcloud/tmp"
DEFAULT_OUTPUT_DIRECTORY = "/home/kae/music/Singles"
DISPLAY_SQL = False

View File

@ -1,7 +1,7 @@
import os
import psutil
from app.config import Config
from config import Config
from datetime import datetime
from pydub import AudioSegment
from mutagen.flac import FLAC
@ -10,6 +10,14 @@ from PyQt5.QtWidgets import QMessageBox
from tinytag import TinyTag
def ask_yes_no(title, question):
"""Ask question; return True for yes, False for no"""
buttonResponse = QMessageBox.question(
self, title, question, QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
def fade_point(audio_segment, fade_threshold=0,
chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE):
"""

View File

@ -25,39 +25,33 @@ from sqlalchemy.orm import backref, relationship, sessionmaker, scoped_session
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
from app.config import Config
from app.helpers import (
from config import Config
from helpers import (
fade_point,
get_audio_segment,
leading_silence,
show_warning,
trailing_silence,
)
from app.log import DEBUG, ERROR
from log import DEBUG, ERROR
# Create session at the global level as per
# https://docs.sqlalchemy.org/en/13/orm/session_basics.html
Base = declarative_base()
Session = scoped_session(sessionmaker())
def db_init():
# Set up database connection
global Session
engine = sqlalchemy.create_engine(
engine = sqlalchemy.create_engine(
f"{Config.MYSQL_CONNECT}?charset=utf8",
encoding='utf-8',
echo=Config.DISPLAY_SQL,
pool_pre_ping=True)
Session.configure(bind=engine)
Base.metadata.create_all(engine)
Session = scoped_session(sessionmaker(bind=engine))
# Create a Session factory
Session = sessionmaker(bind=engine)
Base: DeclarativeMeta = declarative_base()
Base.metadata.create_all(engine)
def db_init():
return
# Database classes

View File

@ -10,7 +10,7 @@ import urllib.parse
from datetime import datetime, timedelta
from log import DEBUG, EXCEPTION
from PyQt5.QtCore import Qt, QTimer, QUrl
from PyQt5.QtCore import QProcess, Qt, QTimer, QUrl
from PyQt5.QtGui import QColor, QFontMetrics, QPainter
from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView
from PyQt5.QtWidgets import (
@ -43,16 +43,19 @@ class ElideLabel(QLabel):
"""
def paintEvent(self, event):
#TODO: V2 check
painter = QPainter(self)
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
painter.drawText(self.rect(), self.alignment(), elided)
#TODO: V2 check
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
#TODO: V2 check
super().__init__(parent)
self.setupUi(self)
@ -85,6 +88,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.start(Config.TIMER_MS)
def add_file(self):
#TODO: V2 check
dlg = QFileDialog()
dlg.setFileMode(QFileDialog.ExistingFiles)
dlg.setViewMode(QFileDialog.Detail)
@ -98,9 +102,10 @@ class Window(QMainWindow, Ui_MainWindow):
# Add to playlist on screen
# If we don't specify "repaint=False", playlist will
# also be saved to database
self.visible_playlist_tab().insert_track(session, track)
self.visible_playlist_tab()._insert_track(session, track)
def set_main_window_size(self):
#TODO: V2 check
"Set size of window from database"
with Session() as session:
@ -115,18 +120,22 @@ class Window(QMainWindow, Ui_MainWindow):
self.setGeometry(x, y, width, height)
def check_audacity(self):
"Warn user if Audacity not running"
#TODO: V2 check
"Offer to run Audacity if not running"
if "audacity" in [i.name() for i in psutil.process_iter()]:
return
helpers.show_warning("Audacity check", "Audacity is not running")
if helpers.ask_yes_no("Audacity not running", "Start Audacity?"):
QProcess.startDetached(Config.AUDACITY_COMMAND, [])
def clear_selection(self):
#TODO: V2 check
if self.visible_playlist_tab():
self.visible_playlist_tab().clearSelection()
def closeEvent(self, event):
#TODO: V2 check
"Don't allow window to close when a track is playing"
if self.music.playing():
@ -165,6 +174,7 @@ class Window(QMainWindow, Ui_MainWindow):
event.accept()
def connect_signals_slots(self):
#TODO: V2 check
self.actionAdd_file.triggered.connect(self.add_file)
self.actionAdd_note.triggered.connect(self.insert_note)
self.action_Clear_selection.triggered.connect(self.clear_selection)
@ -202,6 +212,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick)
def create_playlist(self):
#TODO: V2 check
"Create new playlist"
dlg = QInputDialog(self)
@ -212,9 +223,10 @@ class Window(QMainWindow, Ui_MainWindow):
if ok:
with Session() as session:
playlist_db = Playlists(session, dlg.textValue())
self.load_playlist(session, playlist_db)
self.create_playlist_tab(session, playlist_db)
def change_volume(self, volume):
#TODO: V2 check
"Change player maximum volume"
DEBUG(f"change_volume({volume})")
@ -222,9 +234,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.set_volume(volume)
def close_playlist_tab(self):
#TODO: V2 check
self.close_tab(self.tabPlaylist.currentIndex())
def close_tab(self, index):
#TODO: V2 check
if hasattr(self.tabPlaylist.widget(index), 'is_playlist'):
if self.tabPlaylist.widget(index) == (
self.current_track_playlist_tab):
@ -244,6 +258,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.removeTab(index)
def create_note(self, session, text):
#TODO: V2 check
"""
Create note
@ -265,16 +280,19 @@ class Window(QMainWindow, Ui_MainWindow):
return note
def disable_play_next_controls(self):
#TODO: V2 check
DEBUG("disable_play_next_controls()")
self.actionPlay_next.setEnabled(False)
self.statusbar.showMessage("Play controls: Disabled", 0)
def enable_play_next_controls(self):
#TODO: V2 check
DEBUG("enable_play_next_controls()")
self.actionPlay_next.setEnabled(True)
self.statusbar.showMessage("Play controls: Enabled", 0)
def end_of_track_actions(self):
#TODO: V2 check
"Clean up after track played"
# Set self.playing to False so that tick() doesn't see
@ -301,6 +319,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.enable_play_next_controls()
def ensure_info_tabs(self, title_list):
#TODO: V2 check
"""
Ensure we have info tabs for each of the passed titles
"""
@ -336,6 +355,7 @@ class Window(QMainWindow, Ui_MainWindow):
widget.setUrl(QUrl(url))
def export_playlist_tab(self):
#TODO: V2 check
"Export the current playlist to an m3u file"
if not self.visible_playlist_tab():
@ -374,6 +394,7 @@ class Window(QMainWindow, Ui_MainWindow):
)
def fade(self):
#TODO: V2 check
"Fade currently playing track"
DEBUG("musicmuster:fade()", True)
@ -385,6 +406,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.end_of_track_actions()
def insert_note(self):
#TODO: V2 check
"Add non-track row to playlist"
dlg = QInputDialog(self)
@ -395,28 +417,29 @@ class Window(QMainWindow, Ui_MainWindow):
if ok:
with Session() as session:
note = self.create_note(session, dlg.textValue())
self.visible_playlist_tab().insert_note(session, note)
self.visible_playlist_tab()._insert_note(session, note)
def load_last_playlists(self):
#TODO: V2 check
"Load the playlists that we loaded at end of last session"
with Session() as session:
for playlist_db in Playlists.get_open(session):
self.load_playlist(session, playlist_db)
self.create_playlist_tab(session, playlist_db)
def load_playlist(self, session, playlist_db):
def create_playlist_tab(self, session, playlist_db):
#TODO: V2 check
"""
Take the passed database object, create a playlist display, attach
the database object, get it populated and then add tab.
Take the passed database object, create a playlist tab and add tab
to display.
"""
playlist_tab = PlaylistTab(self)
playlist_db.mark_open(session)
playlist_tab.populate(session, playlist_db)
playlist_tab = PlaylistTab(self, session, playlist_db)
idx = self.tabPlaylist.addTab(playlist_tab, playlist_db.name)
self.tabPlaylist.setCurrentIndex(idx)
def move_selected(self):
#TODO: V2 check
"Move selected rows to another playlist"
# TODO needs refactoring
@ -465,6 +488,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.visible_playlist_tab().remove_rows(rows)
def play_next(self):
#TODO: V2 check
"""
Play next track.
@ -545,35 +569,41 @@ class Window(QMainWindow, Ui_MainWindow):
))
def search_database(self):
#TODO: V2 check
with Session() as session:
dlg = DbDialog(self, session)
dlg.exec()
def open_playlist(self):
#TODO: V2 check
with Session() as session:
playlist_dbs = Playlists.get_closed(session)
dlg = SelectPlaylistDialog(self, playlist_dbs=playlist_dbs)
dlg.exec()
if dlg.plid:
playlist_db = Playlists.get_by_id(session, dlg.plid)
self.load_playlist(session, playlist_db)
self.create_playlist_tab(session, playlist_db)
def select_next_row(self):
#TODO: V2 check
"Select next or first row in playlist"
self.visible_playlist_tab().select_next_row()
def select_played(self):
#TODO: V2 check
"Select all played tracks in playlist"
self.visible_playlist_tab().select_played_tracks()
def select_previous_row(self):
#TODO: V2 check
"Select previous or first row in playlist"
self.visible_playlist_tab().select_previous_row()
def set_next_track(self, next_track_id=None):
#TODO: V2 check
"Set selected track as next"
with Session() as session:
@ -609,11 +639,13 @@ class Window(QMainWindow, Ui_MainWindow):
self.update_headers()
def select_unplayed(self):
#TODO: V2 check
"Select all unplayed tracks in playlist"
self.visible_playlist_tab().select_unplayed_tracks()
def set_tab_colour(self, widget, colour):
#TODO: V2 check
"""
Find the tab containing the widget and set the text colour
"""
@ -622,6 +654,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.tabBar().setTabTextColor(idx, colour)
def song_info_search(self):
#TODO: V2 check
"""
Open browser tabs for Wikipedia, searching for
the first that exists of:
@ -644,6 +677,7 @@ class Window(QMainWindow, Ui_MainWindow):
webbrowser.open(url, new=2)
def stop(self):
#TODO: V2 check
"Stop playing immediately"
DEBUG("musicmuster.stop()")
@ -651,6 +685,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.stop_playing(fade=False)
def stop_playing(self, fade=True):
#TODO: V2 check
"Stop playing current track"
DEBUG(f"musicmuster.stop_playing({fade=})", True)
@ -682,11 +717,13 @@ class Window(QMainWindow, Ui_MainWindow):
self.update_headers()
def test_function(self):
#TODO: V2 check
"Placeholder for test function"
pass
def test_skip_to_end(self):
#TODO: V2 check
"Skip current track to 1 second before silence"
if not self.playing:
@ -695,6 +732,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.set_position(self.current_track.silence_at - 1000)
def test_skip_to_fade(self):
#TODO: V2 check
"Skip current track to 1 second before fade"
if not self.music.playing():
@ -703,6 +741,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.set_position(self.current_track.fade_at - 1000)
def tick(self):
#TODO: V2 check
"""
Update screen
@ -775,6 +814,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.stop_playing()
def update_headers(self):
#TODO: V2 check
"""
Update last / current / next track headers.
Ensure a Wikipedia tab for each title.
@ -811,6 +851,7 @@ class Window(QMainWindow, Ui_MainWindow):
class DbDialog(QDialog):
def __init__(self, parent, session):
#TODO: V2 check
super().__init__(parent)
self.session = session
self.ui = Ui_Dialog()
@ -830,6 +871,7 @@ class DbDialog(QDialog):
self.resize(width, height)
def __del__(self):
#TODO: V2 check
record = Settings.get_int(self.session, "dbdialog_height")
if record.f_int != self.height():
record.update(self.session, {'f_int': self.height()})
@ -839,6 +881,7 @@ class DbDialog(QDialog):
record.update(self.session, {'f_int': self.width()})
def add_selected(self):
#TODO: V2 check
if not self.ui.matchList.selectedItems():
return
@ -847,10 +890,12 @@ class DbDialog(QDialog):
self.add_track(track_id)
def add_selected_and_close(self):
#TODO: V2 check
self.add_selected()
self.close()
def radio_toggle(self):
#TODO: V2 check
"""
Handle switching between searching for artists and searching for
titles
@ -860,6 +905,7 @@ class DbDialog(QDialog):
self.chars_typed(self.ui.searchString.text())
def chars_typed(self, s):
#TODO: V2 check
if len(s) > 0:
if self.ui.radioTitle.isChecked():
matches = Tracks.search_titles(self.session, s)
@ -877,36 +923,42 @@ class DbDialog(QDialog):
self.ui.matchList.addItem(t)
def double_click(self, entry):
#TODO: V2 check
track_id = entry.data(Qt.UserRole)
self.add_track(track_id)
# Select search text to make it easier for next search
self.select_searchtext()
def add_track(self, track_id):
#TODO: V2 check
track = Tracks.get_by_id(self.session, track_id)
# Add to playlist on screen
# If we don't specify "repaint=False", playlist will
# also be saved to database
self.parent().visible_playlist_tab().insert_track(
self.parent().visible_playlist_tab()._insert_track(
self.session, track)
# Select search text to make it easier for next search
self.select_searchtext()
def select_searchtext(self):
#TODO: V2 check
self.ui.searchString.selectAll()
self.ui.searchString.setFocus()
def selection_changed(self):
#TODO: V2 check
if not self.ui.matchList.selectedItems():
return
item = self.ui.matchList.currentItem()
track_id = item.data(Qt.UserRole)
self.ui.dbPath.setText(Tracks.get_path(self.session, track_id))
#TODO: V2 check
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlist_dbs=None):
#TODO: V2 check
super().__init__(parent)
if playlist_dbs is None:
@ -932,6 +984,7 @@ class SelectPlaylistDialog(QDialog):
self.ui.lstPlaylists.addItem(p)
def __del__(self):
#TODO: V2 check
with Session() as session:
record = Settings.get_int(session, "select_playlist_dialog_height")
if record.f_int != self.height():
@ -942,14 +995,17 @@ class SelectPlaylistDialog(QDialog):
record.update(session, {'f_int': self.width()})
def list_doubleclick(self, entry):
#TODO: V2 check
self.plid = entry.data(Qt.UserRole)
self.accept()
def open(self):
#TODO: V2 check
if self.ui.lstPlaylists.selectedItems():
item = self.ui.lstPlaylists.currentItem()
self.plid = item.data(Qt.UserRole)
self.accept()
#TODO: V2 check
def main():
@ -963,5 +1019,6 @@ def main():
EXCEPTION("Unhandled Exception caught by musicmuster.main()")
print(f"{__name__=}")
if __name__ == "__main__":
main()

View File

@ -11,6 +11,7 @@ from PyQt5.QtWidgets import (
QTableWidget,
QTableWidgetItem,
)
from sqlalchemy import inspect
import helpers
import os
@ -28,7 +29,7 @@ from models import (
Tracks,
NoteColours
)
from utilities import create_track_from_file, update_meta
from utilities import create_track_from_file
class PlaylistTab(QTableWidget):
@ -37,7 +38,7 @@ class PlaylistTab(QTableWidget):
cellEditingEnded = QtCore.pyqtSignal()
# Column names
COL_INDEX = 0
COL_AUTOPLAY = COL_USERDATA = 0
COL_MSS = 1
COL_NOTE = 2
COL_TITLE = 2
@ -45,19 +46,23 @@ class PlaylistTab(QTableWidget):
COL_DURATION = 4
COL_START_TIME = 5
COL_END_TIME = 6
COL_LAST_PLAYED = 7
COL_LAST = 7
COL_LAST_PLAYED = COL_LAST = 7
NOTE_COL_SPAN = COL_LAST - COL_NOTE + 1
NOTE_ROW_SPAN = 1
def __init__(self, *args, **kwargs):
# Qt.UserRoles
ROW_METADATA = Qt.UserRole
CONTENT_OBJECT = Qt.UserRole + 1
def __init__(self, parent, session, playlist_db, *args, **kwargs):
super().__init__(*args, **kwargs)
self.id = None
self.name = None
self.is_playlist = True
self.master_process = self.parent()
self.master_process = self.parent() # The MusicMuster process
self.playlist = playlist_db
self.playlist.mark_open(session)
# Set up widget
self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.setAlternatingRowColors(True)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
@ -65,6 +70,7 @@ class PlaylistTab(QTableWidget):
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.setRowCount(0)
self.setColumnCount(8)
# Add header row
item = QtWidgets.QTableWidgetItem()
self.setHorizontalHeaderItem(0, item)
item = QtWidgets.QTableWidgetItem()
@ -84,8 +90,16 @@ class PlaylistTab(QTableWidget):
self.horizontalHeader().setMinimumSectionSize(0)
self._set_column_widths()
self.setHorizontalHeaderLabels(["ID", "Lead", "Title", "Artist",
"Len", "Start", "End", "Last played"])
self.setHorizontalHeaderLabels([
Config.COLUMN_NAME_AUTOPLAY,
Config.COLUMN_NAME_LEADING_SILENCE,
Config.COLUMN_NAME_TITLE,
Config.COLUMN_NAME_ARTIST,
Config.COLUMN_NAME_LENGTH,
Config.COLUMN_NAME_START_TIME,
Config.COLUMN_NAME_END_TIME,
Config.COLUMN_NAME_LAST_PLAYED,
])
self.setDragEnabled(True)
self.setAcceptDrops(True)
@ -97,7 +111,7 @@ class PlaylistTab(QTableWidget):
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setDragDropMode(QAbstractItemView.InternalMove)
# This property holds how the widget shows a context menu
# This property defines how the widget shows a context menu
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
# This signal is emitted when the widget's contextMenuPolicy is
# Qt::CustomContextMenu, and the user has requested a context
@ -113,12 +127,14 @@ class PlaylistTab(QTableWidget):
self.cellEditingStarted.connect(self._cell_edit_started)
self.cellEditingEnded.connect(self._cell_edit_ended)
self.populate(session)
self.current_track_start_time = None
self.played_tracks = []
# ########## Events ##########
def dropEvent(self, event: QDropEvent):
# TODO: V2 check
if not event.isAccepted() and event.source() == self:
drop_row = self._drop_on(event)
@ -165,16 +181,19 @@ class PlaylistTab(QTableWidget):
self.update_display()
def edit(self, index, trigger, event):
# TODO: V2 check
result = super(PlaylistTab, self).edit(index, trigger, event)
if result:
self.cellEditingStarted.emit(index.row(), index.column())
return result
def closeEditor(self, editor, hint):
# TODO: V2 check
super(PlaylistTab, self).closeEditor(editor, hint)
self.cellEditingEnded.emit()
def eventFilter(self, source, event):
# TODO: V2 check
"Used to process context (right-click) menu"
if(event.type() == QtCore.QEvent.MouseButtonPress and # noqa W504
@ -215,6 +234,7 @@ class PlaylistTab(QTableWidget):
# ########## Externally called functions ##########
def close(self, session):
# TODO: V2 check
"Save column widths"
for column in range(self.columnCount()):
@ -224,136 +244,22 @@ class PlaylistTab(QTableWidget):
if record.f_int != self.columnWidth(column):
record.update(session, {'f_int': width})
def insert_note(self, session, note, repaint=True):
"""
Add note to playlist
If a row is selected, add note above. Otherwise, add to end of
playlist.
Return the row number that track is now in.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(f"playlist.inset_note(): row={row}")
# Does note end with a time?
start_time = None
try:
start_time = datetime.strptime(note.note[-9:], " %H:%M:%S").time()
DEBUG(
f"playlist.inset_note(): Note contains valid time={start_time}"
)
except ValueError:
DEBUG(
f"playlist.inset_note(): Note on row {row} ('{note.note}') "
"does not contain valid time"
)
self.insertRow(row)
item = QTableWidgetItem(str(note.id))
self.setItem(row, self.COL_INDEX, item)
titleitem = QTableWidgetItem(note.note)
self.setItem(row, self.COL_NOTE, titleitem)
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
self.NOTE_COL_SPAN)
# Add start/end times or empty items as background
# colour won't be set for columns without items
self._set_row_start_time(row, start_time)
item = QTableWidgetItem()
self.setItem(row, self.COL_END_TIME, item)
item = QTableWidgetItem()
self.setItem(row, self.COL_LAST_PLAYED, item)
self._meta_set_note(row)
# Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
return row
def insert_track(self, session, track, repaint=True):
"""
Insert track into on-screen playlist.
If a row is selected, add track above. Otherwise, add to end of
playlist.
Return the row number that track is now in.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(
f"playlists.insert_track({session=}, {track=}, {repaint=}), "
f"{row=}"
)
self.insertRow(row)
item = QTableWidgetItem(str(track.id))
self.setItem(row, self.COL_INDEX, item)
item = QTableWidgetItem(str(track.start_gap))
if track.start_gap >= 500:
item.setBackground(QColor(Config.COLOUR_LONG_START))
self.setItem(row, self.COL_MSS, item)
titleitem = QTableWidgetItem(track.title)
self.setItem(row, self.COL_TITLE, titleitem)
item = QTableWidgetItem(track.artist)
self.setItem(row, self.COL_ARTIST, item)
item = QTableWidgetItem(helpers.ms_to_mmss(track.duration))
self.setItem(row, self.COL_DURATION, item)
last_playtime = Playdates.last_played(session, track.id)
last_played_str = get_relative_date(last_playtime)
item = QTableWidgetItem(last_played_str)
self.setItem(row, self.COL_LAST_PLAYED, item)
# Add empty start time for now as background
# colour won't be set for columns without items
item = QTableWidgetItem()
self.setItem(row, self.COL_START_TIME, item)
# Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
if not self._track_path_is_readable(track.id):
self._meta_set_unreadable(row)
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
return row
def clear_current(self):
# TODO: V2 check
"Clear current track"
self._meta_clear_current()
self.update_display()
def clear_next(self):
# TODO: V2 check
"""Clear next track"""
self._meta_clear_next()
self.update_display()
def get_next_track_id(self):
"Return next track id"
next_row = self._meta_get_next()
return self._get_row_id(next_row)
def get_selected_row(self):
# TODO: V2 check
"Return row number of first selected row, or None if none selected"
if not self.selectionModel().hasSelection():
@ -362,6 +268,7 @@ class PlaylistTab(QTableWidget):
return self.selectionModel().selectedRows()[0].row()
def get_selected_rows_and_tracks(self):
# TODO: V2 check
"Return a list of selected (rows, track_id) tuples"
if not self.selectionModel().hasSelection():
@ -376,6 +283,7 @@ class PlaylistTab(QTableWidget):
return result
def get_selected_title(self):
# TODO: V2 check
"Return title of selected row or None"
if self.selectionModel().hasSelection():
@ -385,6 +293,7 @@ class PlaylistTab(QTableWidget):
return None
def remove_rows(self, rows):
# TODO: V2 check
"Remove rows passed in rows list"
# Row number will change as we delete rows. We could use
@ -400,6 +309,7 @@ class PlaylistTab(QTableWidget):
self.update_display()
def play_started(self):
# TODO: V2 check
"""
Update current track to be what was next, and determine next track.
Return next track_id.
@ -428,32 +338,26 @@ class PlaylistTab(QTableWidget):
return next_track_id
def play_stopped(self):
# TODO: V2 check
self._meta_clear_current()
self.current_track_start_time = None
self.update_display()
def populate(self, session, playlist_db):
def populate(self, session):
"""
Populate ourself from the passed playlist_db object
Populate ourself from the associated playlist object
We don't mandate that an item will be on its specified row, only
that it will be above larger-numbered row items, and below
lower-numbered ones.
"""
# We don't mandate that an item will be
# on its specified row, only that it will be above
# larger-numbered row items, and below lower-numbered ones.
# That means we need to re-save ourself once loaded to ensure
# database is correct.
# First, save our id for the future
self.id = playlist_db.id
self.name = playlist_db.name
data = []
for t in playlist_db.tracks:
data.append(([t.row], t.tracks))
for n in playlist_db.notes:
data.append(([n.row], n))
for row, track in self.playlist.tracks.items():
data.append(([row], track))
for note in self.playlist.notes:
data.append(([note.row], note))
# Clear playlist
self.setRowCount(0)
@ -462,33 +366,59 @@ class PlaylistTab(QTableWidget):
for i in sorted(data, key=lambda x: x[0]):
item = i[1]
if isinstance(item, Tracks):
self.insert_track(session, item, repaint=False)
self._insert_track(session, item, repaint=False)
elif isinstance(item, Notes):
self.insert_note(session, item, repaint=False)
self._insert_note(session, item, repaint=False)
# Scroll to top
scroll_to = self.item(0, self.COL_INDEX)
scroll_to = self.item(0, self.COL_TITLE)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop)
# We possibly don't need to save the playlist here, but a) row
# numbers may have changed during population, and b) it's cheap
self.save_playlist(session)
self.update_display()
def save_playlist(self, session):
# TODO: V2 check
"""
Save playlist to database.
For notes: check the database entry is correct and update it if
necessary. Playlists:Note is one:many, so there is only one notes
appearance in all playlists.
necessary. Playlists:Note is one:many, so each note may only appear
in one playlist.
For tracks: erase the playlist tracks and recreate. This is much
simpler than trying to correct any Playlists:Tracks many:many
errors.
simpler than trying to implement any Playlists:Tracks many:many
changes.
"""
# We need to add ourself to the session
playlist_db = session.query(Playlists).filter(
Playlists.id == self.id).one()
# TODO: do we need to add ourself to the session?
insp = inspect(self.playlist)
transient = insp.transient
pending = insp.pending
persistent = insp.persistent
deleted = insp.deleted
detached = insp.detached
if transient:
DEBUG("playlist is transient")
session.add(self.playlist)
elif pending:
DEBUG("playlist is pending")
elif persistent:
DEBUG("playlist is persistent")
elif deleted:
DEBUG("playlist is deleted")
elif detached:
DEBUG("playlist is detached")
session.add(self.playlist)
assert inspect(self.playlist) == pending
else:
DEBUG("Can't find state of playlist")
# TODO: hopefully we don't need to do this:
# playlist = session.query(Playlists).filter(
# Playlists.id == TODO: self.id).one()
# Notes first
# Create dictionaries indexed by note_id
@ -498,54 +428,46 @@ class PlaylistTab(QTableWidget):
# PlaylistTab
for row in notes_rows:
note_id = self._get_row_id(row)
if not note_id:
DEBUG(f"(_save_playlist(): no COL_INDEX data in row {row}")
continue
playlist_notes[note_id] = row
playlist_notes[note.id] = self._get_row_content(row)
# Database
for note in playlist_db.notes:
database_notes[note.id] = note.row
for note in self.playlist.notes:
database_notes[note.id] = note
# Notes to add to database
# This should never be needed as notes are added to a specific
# playlist upon creation
for note_id in set(playlist_notes.keys()) - set(database_notes.keys()):
ERROR(
f"_save_playlist(): Note.id={note_id} "
f"missing from playlist {playlist_db} in database"
)
# We don't need to check for notes to add to the database as
# they can't exist in the playlist without being in the database
# and pointing at this playlist.
# Notes to remove from database
for note_id in set(database_notes.keys()) - set(playlist_notes.keys()):
DEBUG(
f"_save_playlist(): Delete note note_id={note_id} "
f"from playlist {playlist_db} in database"
"_save_playlist(): "
f"Delete {note_id=} from {playlist=} in database"
)
Notes.delete_note(session, note_id)
database_notes[note_id].delete_note(session)
# Note rows to update in playlist database
for note_id in set(playlist_notes.keys()) & set(database_notes.keys()):
if playlist_notes[note_id] != database_notes[note_id]:
if playlist_notes[note_id].row != database_notes[note_id].row:
DEBUG(
f"_save_playlist(): Update database note.id {note_id} "
f"from row={database_notes[note_id]} to "
f"row={playlist_notes[note_id]}"
f"_save_playlist(): Update notes row in database "
f"from {database_notes[note_id]=} "
f"to {playlist_notes[note_id]=}"
)
Notes.update_note(session, note_id, playlist_notes[note_id])
database_notes[note_id].update_note(
session, row=playlist_notes[note_id].row)
# Tracks
# Remove all tracks for us in datbase
playlist_db.remove_all_tracks(session)
# Remove all tracks from this playlist
self.playlist.remove_all_tracks(session)
# Iterate on-screen playlist and add tracks back in
for row in range(self.rowCount()):
if row in notes_rows:
continue
playlist_db.add_track(
session, self.id, self._get_row_id(row), row)
playlist.add_track(session, track, row)
def select_next_row(self):
# TODO: V2 check
"""
Select next or first row. Don't select notes. Wrap at last row.
"""
@ -578,6 +500,7 @@ class PlaylistTab(QTableWidget):
self.selectRow(row)
def select_played_tracks(self):
# TODO: V2 check
"""Select all played tracks in playlist"""
# Need to allow multiple rows to be selected
@ -593,6 +516,7 @@ class PlaylistTab(QTableWidget):
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def select_previous_row(self):
# TODO: V2 check
"""
Select previous or last track. Don't select notes. Wrap at first row.
"""
@ -626,6 +550,7 @@ class PlaylistTab(QTableWidget):
self.selectRow(row)
def select_unplayed_tracks(self):
# TODO: V2 check
"Select all unplayed tracks in playlist"
# Need to allow multiple rows to be selected
@ -645,6 +570,7 @@ class PlaylistTab(QTableWidget):
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def set_selected_as_next(self):
# TODO: V2 check
"""
Sets the selected track as the next track.
"""
@ -656,12 +582,10 @@ class PlaylistTab(QTableWidget):
self.update_display()
def update_display(self, clear_selection=True):
# TODO: V2 check
"Set row colours, fonts, etc"
DEBUG(
f"playlist[{self.id}:{self.name}]."
f"_repaint(clear_selection={clear_selection}"
)
DEBUG(f"playlist.update_display [{self.playlist=}]")
with Session() as session:
if clear_selection:
@ -694,6 +618,40 @@ class PlaylistTab(QTableWidget):
# current track.
if row in notes:
# TODO: check whether note has a time
# # Does note end with a time?
# start_time = None
# try:
# start_time = datetime.strptime(note.note[-9:], " %H:%M:%S").time()
# DEBUG(
# f"playlist.inset_note(): Note contains valid time={start_time}"
# )
# except ValueError:
# DEBUG(
# f"playlist.inset_note(): Note on row {row} ('{note.note}') "
# "does not contain valid time"
# )
# self.insertRow(row)
# item = QTableWidgetItem(note)
# self.setItem(row, self.COL_INDEX, item)
# titleitem = QTableWidgetItem(note.note)
# self.setItem(row, self.COL_NOTE, titleitem)
# self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
# self.NOTE_COL_SPAN)
# # Add start/end times or empty items as background
# # colour won't be set for columns without items
# self._set_row_start_time(row, start_time)
# item = QTableWidgetItem()
# self.setItem(row, self.COL_END_TIME, item)
# item = QTableWidgetItem()
# self.setItem(row, self.COL_LAST_PLAYED, item)
# self._meta_set_note(row)
row_time = self._get_row_time(row)
if row_time:
next_start_time = row_time
@ -791,6 +749,7 @@ class PlaylistTab(QTableWidget):
# ########## Internally called functions ##########
def _audacity(self, row):
# TODO: V2 check
"Open track in Audacity. Audacity must be already running"
DEBUG(f"_audacity({row})")
@ -805,6 +764,7 @@ class PlaylistTab(QTableWidget):
open_in_audacity(track.path)
def _calculate_next_start_time(self, session, row, start):
# TODO: V2 check
"Return this row's end time given its start time"
if start is None:
@ -817,10 +777,12 @@ class PlaylistTab(QTableWidget):
return start + timedelta(milliseconds=duration)
def _context_menu(self, pos):
# TODO: V2 check
self.menu.exec_(self.mapToGlobal(pos))
def _copy_path(self, row):
# TODO: V2 check
"""
If passed row is track row, copy the track path to the clipboard.
Otherwise return None.
@ -840,6 +802,7 @@ class PlaylistTab(QTableWidget):
cb.setText(path, mode=cb.Clipboard)
def _cell_changed(self, row, column):
# TODO: V2 check
"Called when cell content has changed"
if not self.editing_cell:
@ -880,18 +843,20 @@ class PlaylistTab(QTableWidget):
else:
track = Tracks.get_by_id(session, row_id)
if column == self.COL_ARTIST:
update_meta(session, track, artist=new)
track.update_artist(session, artist=new)
elif column == self.COL_TITLE:
update_meta(session, track, title=new)
track.update_title(session, title=new)
else:
ERROR("_cell_changed(): unrecognised column")
def _cell_edit_started(self, row, column):
# TODO: V2 check
DEBUG(f"_cell_edit_started({row=}, {column=})")
self.editing_cell = True
self.master_process.disable_play_next_controls()
def _cell_edit_ended(self):
# TODO: V2 check
DEBUG("_cell_edit_ended()")
self.editing_cell = False
@ -902,6 +867,7 @@ class PlaylistTab(QTableWidget):
self.master_process.enable_play_next_controls()
def _delete_rows(self):
# TODO: V2 check
"Delete mutliple rows"
DEBUG("playlist._delete_rows()")
@ -937,6 +903,7 @@ class PlaylistTab(QTableWidget):
self.update_display()
def _drop_on(self, event):
# TODO: V2 check
index = self.indexAt(event.pos())
if not index.isValid():
return self.rowCount()
@ -944,7 +911,13 @@ class PlaylistTab(QTableWidget):
return (index.row() + 1 if self._is_below(event.pos(), index)
else index.row())
def _get_row_content(self, row):
"""Return content associated with this row"""
return self.item(row, self.COL_USERDATA).data(CONTENT_OBJECT)
def _get_row_id(self, row):
# TODO: V2 check
"Return item id as integer from passed row"
if row is None:
@ -962,6 +935,7 @@ class PlaylistTab(QTableWidget):
return None
def _get_row_time(self, row):
# TODO: V2 check
try:
if self.item(row, self.COL_START_TIME):
return datetime.strptime(self.item(
@ -973,6 +947,7 @@ class PlaylistTab(QTableWidget):
return None
def _info_row(self, row):
# TODO: V2 check
"Display popup with info re row"
id = self._get_row_id(row)
@ -1002,7 +977,111 @@ class PlaylistTab(QTableWidget):
info.setDefaultButton(QMessageBox.Cancel)
info.exec()
def _insert_note(self, session, note, repaint=True):
"""
Insert a note to playlist tab.
If a row is selected, add note above. Otherwise, add to end of
playlist.
Return the row number that track is now in.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(f"playlist.inset_note(): row={row}")
self.insertRow(row)
# Add empty items to unused columns because
# colour won't be set for columns without items
item = QTableWidgetItem()
self.setItem(row, self.COL_AUTOPLAY, item)
item = QTableWidgetItem()
self.setItem(row, self.COL_MSS, item)
# Add text of note from title column onwards
titleitem = QTableWidgetItem(note.note)
self.setItem(row, self.COL_NOTE, titleitem)
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
self.NOTE_COL_SPAN)
# Attach note object to row
self._set_row_content(row, note)
# Mark row as a Note row
self._meta_set_note(row)
# Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
return row
def _insert_track(self, session, track, repaint=True):
"""
Insert track into playlist tab.
If a row is selected, add track above. Otherwise, add to end of
playlist.
Return the row number that track is now in.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(
f"playlists.insert_track({session=}, {track=}, {repaint=}), "
f"{row=}"
)
self.insertRow(row)
# Add track details to columns
mss_item = QTableWidgetItem(str(track.start_gap))
if track.start_gap >= 500:
item.setBackground(QColor(Config.COLOUR_LONG_START))
self.setItem(row, self.COL_MSS, mss_item)
title_item = QTableWidgetItem(track.title)
self.setItem(row, self.COL_TITLE, title_item)
artist_item = QTableWidgetItem(track.artist)
self.setItem(row, self.COL_ARTIST, artist_item)
duration_item = QTableWidgetItem(helpers.ms_to_mmss(track.duration))
self.setItem(row, self.COL_DURATION, duration_item)
last_playtime = Playdates.last_played(session, track.id)
last_played_str = get_relative_date(last_playtime)
last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, self.COL_LAST_PLAYED, last_played_item)
# Add empty start and stop time because background
# colour won't be set for columns without items
start_item = QTableWidgetItem()
self.setItem(row, self.COL_START_TIME, start_item)
stop_item = QTableWidgetItem()
self.setItem(row, self.COL_STOP_TIME, stop_item)
# Attach track object to row
self._set_row_content(row, track)
# Mart track if file is unreadable
if not os.access(track.path, os.R_OK):
self._meta_set_unreadable(row)
# Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
return row
def _is_below(self, pos, index):
# TODO: V2 check
rect = self.visualRect(index)
margin = 2
if pos.y() - rect.top() < margin:
@ -1016,6 +1095,7 @@ class PlaylistTab(QTableWidget):
)
def _edit_cell(self, mi):
# TODO: V2 check
"Called when table is double-clicked"
row = mi.row()
@ -1026,6 +1106,7 @@ class PlaylistTab(QTableWidget):
self.editItem(item)
def _find_next_track_row(self, starting_row=None):
# TODO: V2 check
"""
Find next track to play.
@ -1054,11 +1135,13 @@ class PlaylistTab(QTableWidget):
return None
def _meta_clear(self, row):
# TODO: V2 check
"Clear metadata for row"
self._meta_set(row, None)
def _meta_clear_current(self):
# TODO: V2 check
"""
Clear current row if there is one. There may not be if
we've changed playlists
@ -1069,6 +1152,7 @@ class PlaylistTab(QTableWidget):
self._meta_clear(current_row)
def _meta_clear_next(self):
# TODO: V2 check
"""
Clear next row if there is one. There may not be if
we've changed playlists
@ -1079,6 +1163,8 @@ class PlaylistTab(QTableWidget):
self._meta_clear(next_row)
def _meta_find(self, metadata, one=True):
# TODO: V2 check
"""
Search rows for metadata.
@ -1108,31 +1194,37 @@ class PlaylistTab(QTableWidget):
raise AttributeError(f"Multiple '{metadata}' metadata {matches}")
def _meta_get(self, row):
# TODO: V2 check
"Return row metadata"
return self.item(row, self.COL_INDEX).data(Qt.UserRole)
def _meta_get_current(self):
# TODO: V2 check
"Return row marked as current, or None"
return self._meta_find("current")
def _meta_get_next(self):
# TODO: V2 check
"Return row marked as next, or None"
return self._meta_find("next")
def _meta_get_notes(self):
# TODO: V2 check
"Return rows marked as notes, or None"
return self._meta_find("note", one=False)
def _meta_get_unreadable(self):
# TODO: V2 check
"Return rows marked as unreadable, or None"
return self._meta_find("unreadable", one=False)
def _meta_set_current(self, row):
# TODO: V2 check
"Mark row as current track"
old_current = self._meta_get_current()
@ -1141,6 +1233,7 @@ class PlaylistTab(QTableWidget):
self._meta_set(row, "current")
def _meta_set_next(self, row):
# TODO: V2 check
"Mark row as next track"
old_next = self._meta_get_next()
@ -1149,16 +1242,19 @@ class PlaylistTab(QTableWidget):
self._meta_set(row, "next")
def _meta_set_note(self, row):
# TODO: V2 check
"Mark row as note"
self._meta_set(row, "note")
def _meta_set_unreadable(self, row):
# TODO: V2 check
"Mark row as unreadable"
self._meta_set(row, "unreadable")
def _meta_set(self, row, metadata):
# TODO: V2 check
"Set row metadata"
if self.item(row, self.COL_TITLE):
@ -1166,7 +1262,7 @@ class PlaylistTab(QTableWidget):
else:
title = ""
DEBUG(
f"playlist[{self.id}:{self.name}]._meta_set(row={row}, "
f"playlist[{TODO: self.id}:{TODO: self.name}]._meta_set(row={row}, "
f"title={title}, metadata={metadata})"
)
if row is None:
@ -1175,6 +1271,7 @@ class PlaylistTab(QTableWidget):
self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata)
def _set_next(self, row):
# TODO: V2 check
"""
If passed row is track row, check track is readable and, if it is,
set that track as the next track to be played and return track_id.
@ -1201,6 +1298,7 @@ class PlaylistTab(QTableWidget):
return track_id
def _rescan(self, row):
# TODO: V2 check
"""
If passed row is track row, rescan it.
Otherwise return None.
@ -1219,6 +1317,7 @@ class PlaylistTab(QTableWidget):
self._update_row(row, track)
def _select_event(self):
# TODO: V2 check
"""
Called when item selection changes.
If multiple rows are selected, display sum of durations in status bar.
@ -1240,19 +1339,25 @@ class PlaylistTab(QTableWidget):
self.master_process.lblSumPlaytime.setText("")
def _set_column_widths(self):
# TODO: V2 check
# Column widths from settings
with Session() as session:
for column in range(self.columnCount()):
# Only show column 0 in test mode
# TODO: do we need column zero? Has no width ever.
if (column == 0 and not Config.TESTMODE):
self.setColumnWidth(0, 0)
else:
name = f"playlist_col_{str(column)}_width"
record = Settings.get_int(session, name)
if record.f_int is not None:
if record and record.f_int is not None:
self.setColumnWidth(column, record.f_int)
else:
self.setColumnWidth(column,
Config.DEFAULT_COLUMN_WIDTH)
def _set_row_bold(self, row, bold=True):
# TODO: V2 check
boldfont = QFont()
boldfont.setBold(bold)
for j in range(self.columnCount()):
@ -1260,14 +1365,22 @@ class PlaylistTab(QTableWidget):
self.item(row, j).setFont(boldfont)
def _set_row_colour(self, row, colour):
# TODO: V2 check
for j in range(2, self.columnCount()):
if self.item(row, j):
self.item(row, j).setBackground(colour)
def _set_row_content(self, row, content):
"""Set content associated with this row"""
self.item(row, self.COL_USERDATA).setData(CONTENT_OBJECT, content)
def _set_row_not_bold(self, row):
# TODO: V2 check
self._set_row_bold(row, False)
def _set_row_end_time(self, row, time):
# TODO: V2 check
"Set passed row end time to passed time"
try:
time_str = time.strftime("%H:%M:%S")
@ -1277,6 +1390,7 @@ class PlaylistTab(QTableWidget):
self.setItem(row, self.COL_END_TIME, item)
def _set_row_start_time(self, row, time):
# TODO: V2 check
"""Set passed row start time to passed time"""
try:
time_str = time.strftime("%H:%M:%S")
@ -1302,6 +1416,7 @@ class PlaylistTab(QTableWidget):
return False
def _update_row(self, row, track):
# TODO: V2 check
"""
Update the passed row with info from the passed track.
"""

View File

@ -5,10 +5,10 @@ import os
import shutil
import tempfile
from app.config import Config
from app.helpers import show_warning
from app.log import DEBUG, INFO
from app.models import Notes, Playdates, Session, Tracks
from config import Config
from helpers import show_warning
from log import DEBUG, INFO
from models import Notes, Playdates, Session, Tracks
from mutagen.flac import FLAC
from mutagen.mp3 import MP3
from pydub import AudioSegment, effects

19
poetry.lock generated
View File

@ -374,6 +374,21 @@ tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-qt"
version = "4.0.2"
description = "pytest support for PyQt and PySide applications"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pytest = ">=3.0.0"
[package.extras]
dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"]
[[package]]
name = "python-vlc"
version = "3.0.12118"
@ -829,6 +844,10 @@ pytest = [
{file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"},
{file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"},
]
pytest-qt = [
{file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"},
{file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"},
]
python-vlc = [
{file = "python-vlc-3.0.12118.tar.gz", hash = "sha256:566f2f7c303f6800851cacc016df1c6eeec094ad63e0a49d87db9d698094f1fb"},
{file = "python_vlc-3.0.12118-py3-none-any.whl", hash = "sha256:f88be06c6f819a4db2de1c586b193b5df1410ff10fca33b8c6f4e56037c46f7b"},

View File

@ -23,6 +23,7 @@ mypy = "^0.931"
pytest = "^7.0.0"
ipdb = "^0.13.9"
sqlalchemy-stubs = "^0.4"
pytest-qt = "^4.0.2"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@ -46,7 +46,6 @@ def test_notecolours_get_colour_none(session):
def test_notecolours_get_colour_match(session):
note_colour = "#abcdef"
nc = NoteColours(session, substring="sub", colour=note_colour)
assert nc
@ -146,7 +145,6 @@ def test_playdates_remove_track(session):
def test_playlist_create(session):
playlist = Playlists(session, "my playlist")
assert playlist
@ -164,7 +162,6 @@ def test_playlist_add_note(session):
def test_playlist_add_track(session):
# We need a playlist
playlist = Playlists(session, "my playlist")
@ -202,8 +199,27 @@ def test_playlist_tracks(session):
assert tracks[track2_row] == track2
def test_playlist_open_and_close(session):
def test_playlist_notes(session):
# We need a playlist
playlist = Playlists(session, "my playlist")
# We need two notes
note1_text = "note1 text"
note1_row = 11
note1 = Notes(session, playlist.id, note1_row, note1_text)
note2_text = "note2 text"
note2_row = 19
note2 = Notes(session, playlist.id, note2_row, note2_text)
notes = playlist.notes
assert note1_text in [n.note for n in notes]
assert note1_row in [n.row for n in notes]
assert note2_text in [n.note for n in notes]
assert note2_row in [n.row for n in notes]
def test_playlist_open_and_close(session):
# We need a playlist
playlist = Playlists(session, "my playlist")

View File

@ -1,8 +1,10 @@
from app.playlists import PlaylistTab
from app.models import Playlists
def test_init(session):
"""Just check we can create a playlist"""
def test_init(qtbot, session):
"""Just check we can create a playlist_tab"""
playlist = PlaylistTab()
assert playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist)
assert playlist_tab