Rebase dev onto v2_id

This commit is contained in:
Keith Edmunds 2022-03-02 09:24:35 +00:00
parent 281a1d40bf
commit 7f2dd68bce
11 changed files with 733 additions and 605 deletions

View File

@ -39,6 +39,7 @@ class Config(object):
FADE_STEPS = 20
FADE_TIME = 3000
INFO_TAB_TITLE_LENGTH = 15
INFO_TAB_URL = "https://www.wikipedia.org/w/index.php?search=%s"
LOG_LEVEL_STDERR = logging.INFO
LOG_LEVEL_SYSLOG = logging.DEBUG
LOG_NAME = "musicmuster"

View File

@ -13,9 +13,9 @@ 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)
button_reply = QMessageBox.question(None, title, question)
return button_reply == QMessageBox.Yes
def fade_point(audio_segment, fade_threshold=0,

View File

@ -19,7 +19,12 @@ from sqlalchemy import (
func
)
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship, sessionmaker, scoped_session
from sqlalchemy.orm import (
backref,
relationship,
sessionmaker,
scoped_session
)
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
@ -129,7 +134,8 @@ class Notes(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
playlist_id = Column(Integer, ForeignKey('playlists.id'))
playlist = relationship("Playlists", back_populates="notes")
playlist = relationship("Playlists", back_populates="notes",
lazy="joined")
row = Column(Integer, nullable=False)
note = Column(String(256), index=False)
@ -176,7 +182,8 @@ class Playdates(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
lastplayed = Column(DateTime, index=True, default=None)
track_id = Column(Integer, ForeignKey('tracks.id'))
tracks = relationship("Tracks", back_populates="playdates")
tracks = relationship("Tracks", back_populates="playdates",
lazy="joined")
def __init__(self, session, track):
"""Record that track was played"""
@ -226,7 +233,8 @@ class Playlists(Base):
loaded = Column(Boolean, default=True, nullable=False)
notes = relationship("Notes",
order_by="Notes.row",
back_populates="playlist")
back_populates="playlist",
lazy="joined")
tracks = association_proxy('playlist_tracks', 'tracks')
row = association_proxy('playlist_tracks', 'row')
@ -338,7 +346,8 @@ class PlaylistTracks(Base):
Playlists,
backref=backref(
"playlist_tracks",
collection_class=attribute_mapped_collection("row")
collection_class=attribute_mapped_collection("row"),
lazy="joined"
)
)
@ -351,6 +360,39 @@ class PlaylistTracks(Base):
session.add(self)
session.commit()
@staticmethod
def move_track(session, from_playlist_id, row, to_playlist_id):
"""
Move track between playlists. This would be more efficient with
an ORM-enabled UPDATE statement, but this works just fine.
"""
DEBUG(
"PlaylistTracks.move_tracks("
f"{from_playlist_id=}, {rows=}, {to_playlist_id=})"
)
max_row = session.query(func.max(PlaylistTracks.row)).filter(
PlaylistTracks.playlist_id == to_playlist_id).scalar()
if max_row is None:
# Destination playlist is empty; use row 0
new_row = 0
else:
# Destination playlist has tracks; add to end
new_row = max_row + 1
try:
record = session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == from_playlist_id,
PlaylistTracks.row == row).one()
except NoResultFound:
ERROR(
f"No rows matched in query: "
f"PlaylistTracks.playlist_id == {from_playlist_id}, "
f"PlaylistTracks.row == {row}"
)
return
record.playlist_id = to_playlist_id
record.row = new_row
session.commit()
@staticmethod
def next_free_row(session, playlist):
"""Return next free row number"""
@ -409,8 +451,10 @@ class Tracks(Base):
path = Column(String(2048), index=False, nullable=False)
mtime = Column(Float, index=True)
lastplayed = Column(DateTime, index=True, default=None)
playlists = relationship("PlaylistTracks", back_populates="tracks")
playdates = relationship("Playdates", back_populates="tracks")
playlists = relationship("PlaylistTracks", back_populates="tracks",
lazy="joined")
playdates = relationship("Playdates", back_populates="tracks",
lazy="joined")
def __init__(self, session, path):
self.path = path

View File

@ -11,7 +11,7 @@ from datetime import datetime, timedelta
from log import DEBUG, EXCEPTION
from PyQt5.QtCore import QProcess, Qt, QTimer, QUrl
from PyQt5.QtGui import QColor, QFontMetrics, QPainter
from PyQt5.QtGui import QColor
from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView
from PyQt5.QtWidgets import (
QApplication,
@ -27,7 +27,7 @@ import helpers
import music
from config import Config
from models import (db_init, Notes, Playdates, Playlists, PlaylistTracks,
from models import (db_init, Playdates, Playlists, PlaylistTracks,
Session, Settings, Tracks)
from playlists import PlaylistTab
from utilities import create_track_from_file
@ -38,7 +38,6 @@ from ui.main_window_ui import Ui_MainWindow
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
#TODO: V2 check
super().__init__(parent)
self.setupUi(self)
@ -58,7 +57,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.previous_track_position = None
self.spnVolume.setValue(Config.VOLUME_VLC_DEFAULT)
self.menuTest.menuAction().setVisible(Config.TESTMODE)
self.set_main_window_size()
self.lblSumPlaytime = QLabel("")
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
@ -71,7 +69,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.start(Config.TIMER_MS)
def add_file(self):
#TODO: V2 check
# TODO: V2 enahancement to import tracks
dlg = QFileDialog()
dlg.setFileMode(QFileDialog.ExistingFiles)
dlg.setViewMode(QFileDialog.Detail)
@ -85,11 +83,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"
"""Set size of window from database"""
with Session() as session:
record = Settings.get_int(session, "mainwindow_x")
@ -102,7 +99,8 @@ class Window(QMainWindow, Ui_MainWindow):
height = record.f_int or 981
self.setGeometry(x, y, width, height)
def check_audacity(self):
@staticmethod
def check_audacity():
"""Offer to run Audacity if not running"""
if not Config.CHECK_AUDACITY_AT_STARTUP:
@ -115,13 +113,11 @@ class Window(QMainWindow, Ui_MainWindow):
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"
"""Don't allow window to close when a track is playing"""
if self.music.playing():
DEBUG("closeEvent() ignored as music is playing")
@ -159,9 +155,8 @@ 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.actionAdd_note.triggered.connect(self.create_note)
self.action_Clear_selection.triggered.connect(self.clear_selection)
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
@ -177,18 +172,16 @@ class Window(QMainWindow, Ui_MainWindow):
self.select_previous_row)
self.actionSelect_unplayed_tracks.triggered.connect(
self.select_unplayed)
self.actionSetNext.triggered.connect(self.set_next_track)
self.actionSetNext.triggered.connect(
lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
self.actionSkip_next.triggered.connect(self.play_next)
self.actionSkipToEnd.triggered.connect(self.test_skip_to_end)
self.actionSkipToFade.triggered.connect(self.test_skip_to_fade)
self.actionStop.triggered.connect(self.stop)
self.actionTestFunction.triggered.connect(self.test_function)
self.btnAddFile.clicked.connect(self.add_file)
self.btnAddNote.clicked.connect(self.insert_note)
self.btnAddNote.clicked.connect(self.create_note)
self.btnDatabase.clicked.connect(self.search_database)
self.btnFade.clicked.connect(self.fade)
self.btnPlay.clicked.connect(self.play_next)
self.btnSetNext.clicked.connect(self.set_next_track)
self.btnSetNext.clicked.connect(self.this_is_the_next_track)
self.btnSongInfo.clicked.connect(self.song_info_search)
self.btnStop.clicked.connect(self.stop)
self.spnVolume.valueChanged.connect(self.change_volume)
@ -197,8 +190,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick)
def create_playlist(self):
#TODO: V2 check
"Create new playlist"
"""Create new playlist"""
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
@ -207,24 +199,21 @@ class Window(QMainWindow, Ui_MainWindow):
ok = dlg.exec()
if ok:
with Session() as session:
playlist_db = Playlists(session, dlg.textValue())
self.create_playlist_tab(session, playlist_db)
playlist = Playlists(session, dlg.textValue())
self.create_playlist_tab(session, playlist)
def change_volume(self, volume):
#TODO: V2 check
"Change player maximum volume"
"""Change player maximum volume"""
DEBUG(f"change_volume({volume})")
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 hasattr(self.tabPlaylist.widget(index), 'playlist'):
if self.tabPlaylist.widget(index) == (
self.current_track_playlist_tab):
self.statusbar.showMessage(
@ -236,49 +225,32 @@ class Window(QMainWindow, Ui_MainWindow):
return
# It's OK to close this playlist so remove from open playlist list
with Session() as session:
playlist_db = session.query(Playlists).filter(
Playlists.id == self.tabPlaylist.widget(index).id).one()
playlist_db.close(session)
self.tabPlaylist.widget(index).playlist.close(session)
# Close regardless of tab type
self.tabPlaylist.removeTab(index)
def create_note(self, session, text):
#TODO: V2 check
"""
Create note
def create_note(self):
"""Call playlist to create note"""
If a row is selected, set note row to be rows above. Otherwise,
set note row to be end of playlist.
Return note.
"""
row = self.visible_playlist_tab().get_selected_row()
if row is None:
row = self.visible_playlist_tab().rowCount()
DEBUG(f"musicmuster.create_note(text={text}): row={row}")
note = Notes.add_note(
session, self.visible_playlist_tab().id, row, text)
# TODO: this needs to call playlist.add_note now
return note
try:
self.visible_playlist_tab().create_note()
except AttributeError:
# Just return if there's no visible playlist tab
return
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"
"""Clean up after track played"""
# Set self.playing to False so that tick() doesn't see
# player=None and kick off end-of-track actions
@ -287,11 +259,10 @@ class Window(QMainWindow, Ui_MainWindow):
# Clean up metadata
if self.current_track:
self.previous_track = self.current_track
self.current_track = None
if self.current_track_playlist_tab:
self.current_track_playlist_tab.play_stopped()
self.current_track_playlist_tab.clear_current()
self.current_track_playlist_tab = None
self.current_track = None
# Clean up display
self.frame_fade.setStyleSheet("")
@ -303,10 +274,132 @@ class Window(QMainWindow, Ui_MainWindow):
# Enable controls
self.enable_play_next_controls()
def ensure_info_tabs(self, title_list):
def export_playlist_tab(self):
"""Export the current playlist to an m3u file"""
if not self.visible_playlist_tab():
return
# Get output filename
pathspec = QFileDialog.getSaveFileName(
self, 'Save Playlist',
directory=f"{self.visible_playlist_tab().name}.m3u",
filter="M3U files (*.m3u);;All files (*.*)"
)
if not pathspec:
return
path = pathspec[0]
if not path.endswith(".m3u"):
path += ".m3u"
with open(path, "w") as f:
# Required directive on first line
f.write("#EXTM3U\n")
for track in self.playlist.tracks:
f.write(
"#EXTINF:"
f"{int(track.duration / 1000)},"
f"{track.title} - "
f"{track.artist}"
"\n"
f"{track.path}"
"\n"
)
def fade(self):
"""Fade currently playing track"""
DEBUG("musicmuster:fade()", True)
if not self.current_track:
return
self.previous_track_position = self.music.get_position()
self.music.fade()
self.end_of_track_actions()
def insert_note(self):
#TODO: V2 check
"Add non-track row to playlist"
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText("Note:")
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
with Session() as session:
note = self.create_note(session, dlg.textValue())
self.visible_playlist_tab()._insert_note(session, note)
def load_last_playlists(self):
"""Load the playlists that we loaded at end of last session"""
with Session() as session:
for playlist in Playlists.get_open(session):
self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session, playlist):
"""
Take the passed playlist database object, create a playlist tab and
add tab to display.
"""
playlist_tab = PlaylistTab(parent=self,
session=session, playlist=playlist)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx)
def move_selected(self):
"""Move selected rows to another playlist"""
with Session() as session:
playlists = [p for p in Playlists.get_all(session)
if p.id != self.visible_playlist_tab().id]
dlg = SelectPlaylistDialog(self, playlists=playlists)
dlg.exec()
if not dlg.plid:
return
# If destination playlist is visible, we need to add the moved
# tracks to it. If not, they will be automatically loaded when
# the playlistis opened.
destination_visible_playlist_tab = None
for tab in range(self.tabPlaylist.count()):
# Non-playlist tabs won't have a 'playlist' attribute
if not hasattr(self.tabPlaylist.widget(tab), 'playlist'):
continue
if self.tabPlaylist.widget(tab).id == dlg.plid:
destination_visible_playlist_tab = (
self.tabPlaylist.widget(tab))
break
rows = []
for (row, track) in (
self.visible_playlist_tab().get_selected_rows_and_tracks()
):
rows.append(row)
if destination_visible_playlist_tab:
# Insert with repaint=False to not update database
destination_visible_playlist_tab.insert_track(
session, track, repaint=False)
# Update database for both source and destination playlists
PlaylistTracks.move_rows(
session, self.visible_playlist_tab().id, rows, dlg.plid)
# Update destination playlist if visible
if destination_visible_playlist_tab:
destination_visible_playlist_tab.update_display()
# Update source playlist
self.visible_playlist_tab().remove_rows(rows)
def open_info_tab(self, title_list):
"""
Ensure we have info tabs for each of the passed titles
Called from update_headers
"""
for title in title_list:
@ -332,148 +425,15 @@ class Window(QMainWindow, Ui_MainWindow):
idx, title[:Config.INFO_TAB_TITLE_LENGTH])
del self.info_tabs[old_title]
else:
# Create a new tab for this title
widget = self.info_tabs[title] = QWebView()
self.tabPlaylist.addTab(
widget, title[:Config.INFO_TAB_TITLE_LENGTH])
str = urllib.parse.quote_plus(title)
url = f"https://www.wikipedia.org/w/index.php?search={str}"
txt = urllib.parse.quote_plus(title)
url = Config.INFO_TAB_URL % txt
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():
return
# Get output filename
pathspec = QFileDialog.getSaveFileName(
self, 'Save Playlist',
directory=f"{self.visible_playlist_tab().name}.m3u",
filter="M3U files (*.m3u);;All files (*.*)"
)
if not pathspec:
return
path = pathspec[0]
if not path.endswith(".m3u"):
path += ".m3u"
# Get playlist db object
with Session() as session:
playlist_db = Playlists.get_by_id(
session, self.visible_playlist_tab().id)
with open(path, "w") as f:
# Required directive on first line
f.write("#EXTM3U\n")
for track in playlist_db.tracks:
f.write(
"#EXTINF:"
f"{int(track.duration / 1000)},"
f"{track.title} - "
f"{track.artist}"
"\n"
f"{track.path}"
"\n"
)
def fade(self):
#TODO: V2 check
"Fade currently playing track"
DEBUG("musicmuster:fade()", True)
if not self.current_track:
return
self.previous_track_position = self.music.fade()
self.end_of_track_actions()
def insert_note(self):
#TODO: V2 check
"Add non-track row to playlist"
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText("Note:")
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
with Session() as session:
note = self.create_note(session, dlg.textValue())
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.create_playlist_tab(session, playlist_db)
def create_playlist_tab(self, session, playlist):
#TODO: V2 check
"""
Take the passed playlist database object, create a playlist tab and
add tab to display.
"""
playlist_tab = PlaylistTab(parent=self,
session=session, playlist=playlist)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx)
def move_selected(self):
#TODO: V2 check
"Move selected rows to another playlist"
# TODO needs refactoring
with Session() as session:
playlist_dbs = [p for p in Playlists.get_all(session)
if p.id != self.visible_playlist_tab().id]
dlg = SelectPlaylistDialog(self, playlist_dbs=playlist_dbs)
dlg.exec()
if not dlg.plid:
return
# If destination playlist is visible, we need to add the moved
# tracks to it. If not, they will be automatically loaded when
# the playlistis opened.
destination_visible_playlist_tab = None
for tab in range(self.tabPlaylist.count()):
# Non-playlist tabs won't have ids
if not hasattr(self.tabPlaylist.widget(tab), 'id'):
continue
if self.tabPlaylist.widget(tab).id == dlg.plid:
destination_visible_playlist_tab = (
self.tabPlaylist.widget(tab))
break
rows = []
for (row, track) in (
self.visible_playlist_tab().get_selected_rows_and_tracks()
):
rows.append(row)
if destination_visible_playlist_tab:
# Insert with repaint=False to not update database
destination_visible_playlist_tab.insert_track(
session, track, repaint=False)
# Update database for both source and destination playlists
PlaylistTracks.move_track(
session, self.visible_playlist_tab().id, row, dlg.plid)
# Update destination playlist if visible
if destination_visible_playlist_tab:
destination_visible_playlist_tab.update_display()
# Update source playlist
self.visible_playlist_tab().remove_rows(rows)
def play_next(self):
#TODO: V2 check
"""
Play next track.
@ -481,10 +441,10 @@ class Window(QMainWindow, Ui_MainWindow):
If there's currently a track playing, fade it.
Move next track to current track.
Play (new) current.
Update playlist "current track" metadata
Tell playlist to update "current track" metadata. This will also
trigger a call to
Cue up next track in playlist if there is one.
Tell database to record it as played
Remember it was played for this session
Update metadata and headers, and repaint
"""
@ -507,36 +467,13 @@ class Window(QMainWindow, Ui_MainWindow):
# Play next track
self.current_track = self.next_track
self.next_track = None
self.current_track_playlist_tab = self.next_track_playlist_tab
self.next_track_playlist_tab = None
self.set_tab_colour(self.current_track_playlist_tab,
QColor(Config.COLOUR_CURRENT_TAB))
self.next_track = None
self.next_track_playlist_tab = None
DEBUG(
"musicmuster.play_next: calling music.play("
f"{self.current_track.path=})"
)
self.music.play(self.current_track.path)
# Update metadata
# Get next track for this playlist. May be None if there is
# no automatic next track, and may later be overriden by
# user selecting a different track on this or another
# playlist.
***KAE won't return next_track_id *** = self.current_track_playlist_tab.play_started()
if next_track_id is not None:
self.next_track = Tracks.get_by_id(session, next_track_id)
self.next_track_playlist_tab = self.current_track_playlist_tab
else:
self.next_track = self.next_track_playlist_tab = None
if self.next_track_playlist_tab and (
self.current_track_playlist_tab !=
self.next_track_playlist_tab):
self.set_tab_colour(self.next_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
# Tell database to record it as played
Playdates(session, self.current_track)
@ -554,83 +491,40 @@ 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)
playlists = Playlists.get_closed(session)
dlg = SelectPlaylistDialog(self, playlists=playlists)
dlg.exec()
if dlg.plid:
playlist_db = Playlists.get_by_id(session, dlg.plid)
self.create_playlist_tab(session, playlist_db)
playlist = Playlists.get_by_id(session, dlg.plid)
self.create_playlist_tab(session, playlist)
def select_next_row(self):
#TODO: V2 check
"Select next or first row in playlist"
"""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"
"""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"
"""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:
if not next_track_id:
next_track_id = (
self.visible_playlist_tab().set_selected_as_next())
if not next_track_id:
return
# The next track has been selected on the currently-visible
# playlist. However, there may already be a 'next track'
# selected on another playlist that the user is overriding,
# in which case we need to reset that playlist.
if self.next_track_playlist_tab != self.visible_playlist_tab():
# We need to reset the ex-next-track playlist
if self.next_track_playlist_tab:
self.next_track_playlist_tab.clear_next()
# Reset tab colour if it NOT the current playing tab
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(self.next_track_playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
self.next_track_playlist_tab = self.visible_playlist_tab()
# self.next_track_playlist_tab is now set to correct
# playlist
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(self.next_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
self.next_track = Tracks.get_by_id(session, next_track_id)
self.update_headers()
def select_unplayed(self):
#TODO: V2 check
"Select all unplayed tracks in playlist"
"""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
"""
@ -639,9 +533,8 @@ 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
Open browser tab for Wikipedia, searching for
the first that exists of:
- selected track
- next track
@ -657,24 +550,23 @@ class Window(QMainWindow, Ui_MainWindow):
title = self.current_track.title
if title:
# Wikipedia
str = urllib.parse.quote_plus(title)
url = f"https://www.wikipedia.org/w/index.php?search={str}"
txt = urllib.parse.quote_plus(title)
url = Config.INFO_TAB_URL % txt
webbrowser.open(url, new=2)
def stop(self):
#TODO: V2 check
"Stop playing immediately"
"""Stop playing immediately"""
DEBUG("musicmuster.stop()")
self.stop_playing(fade=False)
def stop_playing(self, fade=True):
#TODO: V2 check
"Stop playing current track"
"""Stop playing current track"""
DEBUG(f"musicmuster.stop_playing({fade=})", True)
# Set tab colour
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))
@ -701,32 +593,41 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.stop()
self.update_headers()
def test_function(self):
#TODO: V2 check
"Placeholder for test function"
def this_is_the_next_track(self, playlist_tab, track):
"""
This is notification from a playlist tab that it holds the next
track to be played.
"""
pass
# The next track has been selected on the playlist_tab
# playlist. However, there may already be a 'next track'
# selected on another playlist that the user is overriding,
# in which case we need to reset that playlist.
if self.next_track_playlist_tab != playlist_tab:
# We need to reset the ex-next-track playlist
if self.next_track_playlist_tab:
self.next_track_playlist_tab.clear_next()
# Reset tab colour if it NOT the current playing tab
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(
self.next_track_playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
self.next_track_playlist_tab = playlist_tab
# self.next_track_playlist_tab is now set to correct playlist
# Set the colour of the next playlist tab if it isn't the
# currently-playing tab
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(
self.next_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
def test_skip_to_end(self):
#TODO: V2 check
"Skip current track to 1 second before silence"
self.next_track = track
if not self.playing:
return
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():
return
self.music.set_position(self.current_track.fade_at - 1000)
self.update_headers()
def tick(self):
#TODO: V2 check
"""
Update screen
@ -799,7 +700,6 @@ 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.
@ -831,12 +731,13 @@ class Window(QMainWindow, Ui_MainWindow):
except AttributeError:
self.hdrNextTrack.setText("")
self.ensure_info_tabs(titles)
self.open_info_tab(titles)
class DbDialog(QDialog):
"""Select track from database"""
def __init__(self, parent, session):
#TODO: V2 check
super().__init__(parent)
self.session = session
self.ui = Ui_Dialog()
@ -846,7 +747,7 @@ class DbDialog(QDialog):
self.ui.btnClose.clicked.connect(self.close)
self.ui.matchList.itemDoubleClicked.connect(self.double_click)
self.ui.matchList.itemSelectionChanged.connect(self.selection_changed)
self.ui.radioTitle.toggled.connect(self.radio_toggle)
self.ui.radioTitle.toggled.connect(self.title_artist_toggle)
self.ui.searchString.textEdited.connect(self.chars_typed)
record = Settings.get_int(self.session, "dbdialog_width")
@ -856,7 +757,6 @@ 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()})
@ -866,21 +766,18 @@ 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
item = self.ui.matchList.currentItem()
track_id = item.data(Qt.UserRole)
self.add_track(track_id)
track = item.data(Qt.UserRole)
self.add_track(track)
def add_selected_and_close(self):
#TODO: V2 check
self.add_selected()
self.close()
def radio_toggle(self):
#TODO: V2 check
def title_artist_toggle(self):
"""
Handle switching between searching for artists and searching for
titles
@ -890,7 +787,6 @@ 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)
@ -904,49 +800,40 @@ class DbDialog(QDialog):
f"{track.title} - {track.artist} "
f"[{helpers.ms_to_mmss(track.duration)}]"
)
t.setData(Qt.UserRole, track.id)
t.setData(Qt.UserRole, track)
self.ui.matchList.addItem(t)
def double_click(self, entry):
#TODO: V2 check
track_id = entry.data(Qt.UserRole)
self.add_track(track_id)
track = entry.data(Qt.UserRole)
self.add_track(track)
# 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)
def add_track(self, track):
# 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
track = item.data(Qt.UserRole)
self.ui.dbPath.setText(track.path)
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlist_dbs=None):
#TODO: V2 check
def __init__(self, parent=None, playlists=None):
super().__init__(parent)
if playlist_dbs is None:
if playlists is None:
return
self.ui = Ui_dlgSelectPlaylist()
self.ui.setupUi(self)
@ -962,14 +849,13 @@ class SelectPlaylistDialog(QDialog):
height = record.f_int or 600
self.resize(width, height)
for (plid, plname) in [(a.id, a.name) for a in playlist_dbs]:
for (plid, plname) in [(a.id, a.name) for a in playlists]:
p = QListWidgetItem()
p.setText(plname)
p.setData(Qt.UserRole, plid)
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():
@ -980,17 +866,14 @@ 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():

View File

@ -11,7 +11,7 @@ from PyQt5.QtWidgets import (
QMenu,
QMessageBox,
QTableWidget,
QTableWidgetItem,
QTableWidgetItem, QInputDialog,
)
from sqlalchemy import inspect
@ -136,11 +136,14 @@ class PlaylistTab(QTableWidget):
self.cellEditingEnded.connect(self._cell_edit_ended)
# Now load our tracks and notes
self.populate(session)
self._populate(session)
self.current_track_start_time = None
def __repr__(self):
return f"<PlaylistTab(id={self.id}, name={self.name}>"
return (
f"<PlaylistTab(id={self.playlist.id}, "
f"name={self.playlist.name}>"
)
# ########## Events ##########
@ -189,8 +192,8 @@ class PlaylistTab(QTableWidget):
self.save_playlist(session)
self.update_display()
def edit(self, index):
result = super(PlaylistTab, self).edit(index)
def edit(self, index, trigger, event):
result = super(PlaylistTab, self).edit(index, trigger, event)
if result:
self.cellEditingStarted.emit(index.row(), index.column())
return result
@ -252,18 +255,35 @@ class PlaylistTab(QTableWidget):
event.accept()
def clear_current(self):
"""Clear current track"""
self._meta_clear_current()
self.update_display()
def clear_next(self):
"""Clear next track"""
self._meta_clear_next()
self.update_display()
def create_note(self):
"""
Create note
If a row is selected, set note row to be rows above. Otherwise,
set note row to be end of playlist.
"""
row = self.get_selected_row()
if not row:
row = self.rowCount()
# Get note text
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText("Note:")
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
with Session() as session:
note = Notes(session, self.playlist.id, row, dlg.textValue())
self._insert_note(session, note, row, True)
def get_selected_row(self):
"""Return row number of first selected row, or None if none selected"""
@ -294,6 +314,70 @@ class PlaylistTab(QTableWidget):
else:
return None
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)
# Put an item in COL_USERDATA for later
item = QTableWidgetItem()
self.setItem(row, self.COL_USERDATA, item)
# Add track details to columns
mss_item = QTableWidgetItem(str(track.start_gap))
if track.start_gap and 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_END_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(title_item, QAbstractItemView.PositionAtCenter)
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
return row
def remove_rows(self, rows):
"""Remove rows passed in rows list"""
@ -337,48 +421,6 @@ class PlaylistTab(QTableWidget):
self.current_track_start_time = None
self.update_display()
def populate(self, session):
"""
Populate 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.
"""
data = []
# Make sure the database object is usable
insp = inspect(self.playlist)
if insp.detached:
session.add(self.playlist)
assert insp.persistent
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)
# Now add data in row order
for i in sorted(data, key=lambda x: x[0]):
item = i[1]
if isinstance(item, Tracks):
self._insert_track(session, item, repaint=False)
elif isinstance(item, Notes):
self._insert_note(session, item, repaint=False)
# Scroll to top
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 row
# numbers may have changed during population, and it's cheap to do
self.save_playlist(session)
self.update_display()
def save_playlist(self, session):
"""
Save playlist to database.
@ -519,17 +561,32 @@ class PlaylistTab(QTableWidget):
self._select_tracks(played=False)
def set_selected_as_next(self):
def set_next_track(self, row):
"""
Sets the selected track as the next track.
Sets the passed row as the next track.
"""
if len(self.selectedItems()) != 1:
if row in self._meta_get_notes():
return
self._set_next(self.currentRow())
# Update row metadata
self._set_next(row)
# Notify parent
track = self._get_row_object(row)
self.parent.this_is_the_next_track(self, track)
# Show track as next
self.update_display()
def set_selected_as_next(self):
"""Sets the select track as next to play"""
row = self.get_selected_row()
if row is None:
return None
self.set_next_track(row)
def update_display(self, clear_selection=True):
"""Set row colours, fonts, etc"""
@ -823,7 +880,7 @@ class PlaylistTab(QTableWidget):
try:
return datetime.strptime(
text[-Config.NOTE_TIME_FORMAT:],
text[-len(Config.NOTE_TIME_FORMAT):],
Config.NOTE_TIME_FORMAT
)
except ValueError:
@ -832,7 +889,20 @@ class PlaylistTab(QTableWidget):
def _get_row_object(self, row):
"""Return content associated with this row"""
return self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
# row_item = self.item(row, self.COL_USERDATA)
# obj = row_item.data(self.CONTENT_OBJECT)
# insp = inspect(obj)
# with Session() as session:
# # x = session.query(type(obj)).populate_existing().get(obj.id)
# x = session.get(type(obj), obj.id, populate_existing=True)
# insp = inspect(x)
# insp = inspect(obj)
# insp = inspect(x)
# insp = inspect(obj)
# print(obj)
# return obj
obj = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
return obj
def _info_row(self, row):
"""Display popup with info re row"""
@ -859,19 +929,15 @@ class PlaylistTab(QTableWidget):
info.setDefaultButton(QMessageBox.Cancel)
info.exec()
def _insert_note(self, session, note, repaint=True):
def _insert_note(self, session, note, row=None, repaint=True):
"""
Insert a note to playlist tab.
If a row is selected, add note above. Otherwise, add to end of
If a row is given, 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:
if row is None:
row = self.rowCount()
DEBUG(f"playlist.inset_note(): row={row}")
@ -898,72 +964,6 @@ class PlaylistTab(QTableWidget):
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)
# Put an item in COL_USERDATA for later
item = QTableWidgetItem()
self.setItem(row, self.COL_USERDATA, item)
# Add track details to columns
mss_item = QTableWidgetItem(str(track.start_gap))
if track.start_gap and 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_END_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(title_item, QAbstractItemView.PositionAtCenter)
if repaint:
self.save_playlist(session)
self.update_display(clear_selection=False)
return row
def _is_below(self, pos, index):
rect = self.visualRect(index)
margin = 2
@ -1091,8 +1091,9 @@ class PlaylistTab(QTableWidget):
matches = []
for row in range(self.rowCount()):
if self._meta_get(row) & metadata:
matches.append(row)
if self._meta_get(row):
if self._meta_get(row) & metadata:
matches.append(row)
if not one:
return matches
@ -1114,7 +1115,11 @@ class PlaylistTab(QTableWidget):
if row is None:
raise ValueError(f"_meta_set_attribute({row=}, {attribute=})")
new_metadata = self._meta_get(row) | attribute
current_metadata = self._meta_get(row)
if not current_metadata:
new_metadata = attribute
else:
new_metadata = self._meta_get(row) | attribute
self.item(row, self.COL_USERDATA).setData(
self.ROW_METADATA, new_metadata)
@ -1145,35 +1150,51 @@ class PlaylistTab(QTableWidget):
self._meta_set_attribute(row, RowMeta.UNREADABLE)
def _set_next(self, row):
def _populate(self, session):
"""
If passed row is track row, check track is readable and, if it is:
- mark that track as the next track to be played
- notify musicmuster
- return track
Populate from the associated playlist object
Otherwise, return None.
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.
"""
DEBUG(f"_set_next({row=})")
data = []
if row in self._meta_get_notes():
return None
# Make sure the database object is usable
insp = inspect(self.playlist)
if insp.detached:
session.add(self.playlist)
assert insp.persistent
track = self._get_row_object(row)
if not track:
return None
for row, track in self.playlist.tracks.items():
insp = inspect(track)
data.append(([row], track))
# Add track to session to expose attributes
session.add(track)
for note in self.playlist.notes:
data.append(([note.row], note))
if self._track_is_readable(track):
self._meta_set_next(row)
self.parent.set_next_track(track)
else:
self._meta_set_unreadable(row)
track = None
# Clear playlist
self.setRowCount(0)
# Now add data in row order
for i in sorted(data, key=lambda x: x[0]):
item = i[1]
if isinstance(item, Tracks):
self.insert_track(session, item, repaint=False)
elif isinstance(item, Notes):
self._insert_note(session, item, repaint=False)
# Scroll to top
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 row
# numbers may have changed during population, and it's cheap to do
self.save_playlist(session)
self.update_display()
return track
def _rescan(self, row):
"""
If passed row is track row, rescan it.
@ -1197,10 +1218,22 @@ class PlaylistTab(QTableWidget):
If multiple rows are selected, display sum of durations in status bar.
"""
rows = set([item.row() for item in self.selectedItems()])
note_rows = self._meta_get_notes()
ms = sum([self._get_row_object(row).duration
for row in rows if row not in note_rows])
row_set = set([item.row() for item in self.selectedItems()])
note_row_set = set(self._meta_get_notes())
track_rows = list(row_set - note_row_set)
ms = 0
with Session() as session:
# tracks = [self._get_row_object(row) for row in track_rows]
# # Add all tracks to the session
# [session.add(track) for track in tracks]
for row in track_rows:
track = self._get_row_object(row)
insp = inspect(track)
session.add(track)
insp = inspect(track)
ms += track.duration
# ms = sum([track.duration for track in tracks])
# Only paint message if there are selected track rows
if ms > 0:
self.parent.lblSumPlaytime.setText(
@ -1219,6 +1252,35 @@ class PlaylistTab(QTableWidget):
else:
self.setColumnWidth(column, Config.DEFAULT_COLUMN_WIDTH)
def _set_next(self, row):
"""
If passed row is track row, check track is readable and, if it is:
- mark that track as the next track to be played
- notify musicmuster
- return track
Otherwise, return None.
"""
DEBUG(f"_set_next({row=})")
if row in self._meta_get_notes():
return None
track = self._get_row_object(row)
if not track:
return None
if self._track_is_readable(track):
self._meta_set_next(row)
self.parent.this_is_the_next_track(track)
else:
self._meta_set_unreadable(row)
track = None
self.update_display()
return track
def _set_row_bold(self, row, bold=True):
boldfont = QFont()
boldfont.setBold(bold)

View File

@ -794,19 +794,9 @@ border: 1px solid rgb(85, 87, 83);</string>
<addaction name="separator"/>
<addaction name="actionSetNext"/>
</widget>
<widget class="QMenu" name="menuTest">
<property name="title">
<string>TestMo&amp;de</string>
</property>
<addaction name="actionTestFunction"/>
<addaction name="separator"/>
<addaction name="actionSkipToFade"/>
<addaction name="actionSkipToEnd"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuPlaylist"/>
<addaction name="menu_Tracks"/>
<addaction name="menuTest"/>
</widget>
<widget class="QStatusBar" name="statusbar">
<property name="enabled">

View File

@ -356,8 +356,6 @@ class Ui_MainWindow(object):
self.menuPlaylist.setObjectName("menuPlaylist")
self.menu_Tracks = QtWidgets.QMenu(self.menubar)
self.menu_Tracks.setObjectName("menu_Tracks")
self.menuTest = QtWidgets.QMenu(self.menubar)
self.menuTest.setObjectName("menuTest")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setEnabled(True)
@ -467,14 +465,9 @@ class Ui_MainWindow(object):
self.menu_Tracks.addAction(self.action_Resume_previous)
self.menu_Tracks.addSeparator()
self.menu_Tracks.addAction(self.actionSetNext)
self.menuTest.addAction(self.actionTestFunction)
self.menuTest.addSeparator()
self.menuTest.addAction(self.actionSkipToFade)
self.menuTest.addAction(self.actionSkipToEnd)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuPlaylist.menuAction())
self.menubar.addAction(self.menu_Tracks.menuAction())
self.menubar.addAction(self.menuTest.menuAction())
self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1)
@ -513,7 +506,6 @@ class Ui_MainWindow(object):
self.menuFile.setTitle(_translate("MainWindow", "Fi&le"))
self.menuPlaylist.setTitle(_translate("MainWindow", "Pla&ylist"))
self.menu_Tracks.setTitle(_translate("MainWindow", "&Tracks"))
self.menuTest.setTitle(_translate("MainWindow", "TestMo&de"))
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
self.actionSkip_next.setText(_translate("MainWindow", "Skip to &next"))

131
poetry.lock generated
View File

@ -158,11 +158,11 @@ lingua = ["lingua"]
[[package]]
name = "markupsafe"
version = "2.0.1"
version = "2.1.0"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[[package]]
name = "matplotlib-inline"
@ -187,7 +187,7 @@ python-versions = ">=3.5, <4"
name = "mypy"
version = "0.931"
description = "Optional static typing for Python"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
@ -204,7 +204,7 @@ python2 = ["typed-ast (>=1.4.0,<2)"]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
category = "main"
optional = false
python-versions = "*"
@ -355,7 +355,7 @@ python-versions = "*"
[[package]]
name = "pytest"
version = "7.0.0"
version = "7.0.1"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
@ -476,7 +476,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.7"
@ -495,7 +495,7 @@ test = ["pytest"]
name = "typing-extensions"
version = "4.0.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
@ -614,75 +614,46 @@ mako = [
{file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
{file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"},
{file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"},
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"},
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"},
{file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"},
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"},
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"},
{file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"},
{file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"},
{file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"},
{file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"},
{file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"},
{file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"},
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"},
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"},
{file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"},
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"},
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"},
{file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"},
{file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"},
{file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"},
{file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"},
{file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"},
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"},
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"},
{file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"},
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"},
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"},
{file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"},
{file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"},
{file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"},
{file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"},
]
matplotlib-inline = [
{file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
@ -841,8 +812,8 @@ pyqtwebengine-qt5 = [
{file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:24231f19e1595018779977de6722b5c69f3d03f34a5f7574ff21cd1e764ef76d"},
]
pytest = [
{file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"},
{file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"},
{file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"},
{file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"},
]
pytest-qt = [
{file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"},
@ -922,6 +893,6 @@ wcwidth = [
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
typing-extensions = [
{file = "typing_extensions-4.1.0-py3-none-any.whl", hash = "sha256:c13180fbaa7cd97065a4915ceba012bdb31dc34743e63ddee16360161d358414"},
{file = "typing_extensions-4.1.0.tar.gz", hash = "sha256:ba97c5143e5bb067b57793c726dd857b1671d4b02ced273ca0538e71ff009095"},
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
]

View File

@ -17,6 +17,7 @@ psutil = "^5.9.0"
PyQtWebEngine = "^5.15.5"
pydub = "^0.25.1"
PyQt5-sip = "^12.9.1"
mypy = "^0.931"
[tool.poetry.dev-dependencies]
mypy = "^0.931"

View File

@ -7,6 +7,7 @@ from app.models import (
Notes,
Playdates,
Playlists,
PlaylistTracks,
Tracks,
)
@ -306,6 +307,42 @@ def test_playlist_get_track_playlists(session):
assert p2_name not in [a.playlist.name for a in playlists_track2]
def test_playlisttracks_move_track(session):
# We need two playlists
p1_name = "playlist one"
p2_name = "playlist two"
playlist1 = Playlists(session, p1_name)
playlist2 = Playlists(session, p2_name)
# Need two tracks
track1_row = 17
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
track2_row = 29
track2_path = "/m/n/o"
track2 = Tracks(session, track2_path)
track1 = Tracks(session, track1_path)
# Add both to playlist1 and check
playlist1.add_track(session, track1, track1_row)
playlist1.add_track(session, track2, track2_row)
tracks = playlist1.tracks
assert tracks[track1_row] == track1
assert tracks[track2_row] == track2
# Move track2 to playlist2 and check
PlaylistTracks.move_track(
session, playlist1.id, track2_row, playlist2.id)
tracks1 = playlist1.tracks
tracks2 = playlist2.tracks
assert len(tracks1) == 1
assert len(tracks2) == 1
assert tracks1[track1_row] == track1
assert tracks2[track2_row] == track2
def test_tracks_get_all_paths(session):
# Need two tracks
track1_path = "/a/b/c"

View File

@ -1,5 +1,9 @@
from PyQt5.QtCore import Qt
from app.playlists import Notes, PlaylistTab, Tracks
from app.models import Playlists
# from musicmuster import Window
from musicmuster import Window
def test_init(qtbot, session):
@ -26,7 +30,7 @@ def test_save_and_restore(qtbot, session):
# Add a track
track_path = "/a/b/c"
track = Tracks(session, track_path)
playlist_tab._insert_track(session, track)
playlist_tab.insert_track(session, track)
# Save playlist
playlist_tab.save_playlist(session)
@ -37,3 +41,146 @@ def test_save_and_restore(qtbot, session):
retrieved_playlist = playlists[0]
assert track_path in [a.path for a in retrieved_playlist.tracks.values()]
assert note_text in [a.note for a in retrieved_playlist.notes]
def test_meta_all_clear(qtbot, session):
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
track3_path = "/h/i/j"
track3 = Tracks(session, track3_path)
playlist_tab.insert_track(session, track3)
assert playlist_tab._meta_get_current() is None
assert playlist_tab._meta_get_next() is None
assert playlist_tab._meta_get_notes() == []
assert playlist_tab._meta_get_played() == []
assert len(playlist_tab._meta_get_unreadable()) == 3
def test_meta(qtbot, session):
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
track3_path = "/h/i/j"
track3 = Tracks(session, track3_path)
playlist_tab.insert_track(session, track3)
assert len(playlist_tab._meta_get_unreadable()) == 3
assert playlist_tab._meta_get_played() == []
assert playlist_tab._meta_get_current() is None
assert playlist_tab._meta_get_next() is None
assert playlist_tab._meta_get_notes() == []
playlist_tab._meta_set_played(0)
assert playlist_tab._meta_get_played() == [0]
assert playlist_tab._meta_get_current() is None
assert playlist_tab._meta_get_next() is None
assert playlist_tab._meta_get_notes() == []
# Add a note
note_text = "my note"
note_row = 7 # will be added as row 3
note = Notes(session, playlist.id, note_row, note_text)
playlist_tab._insert_note(session, note)
assert playlist_tab._meta_get_played() == [0]
assert playlist_tab._meta_get_current() is None
assert playlist_tab._meta_get_next() is None
assert playlist_tab._meta_get_notes() == [3]
playlist_tab._meta_set_next(1)
assert playlist_tab._meta_get_played() == [0]
assert playlist_tab._meta_get_current() is None
assert playlist_tab._meta_get_next() == 1
assert playlist_tab._meta_get_notes() == [3]
playlist_tab._meta_set_current(2)
assert playlist_tab._meta_get_played() == [0]
assert playlist_tab._meta_get_current() == 2
assert playlist_tab._meta_get_next() == 1
assert playlist_tab._meta_get_notes() == [3]
playlist_tab._meta_clear_played(0)
assert playlist_tab._meta_get_played() == []
assert playlist_tab._meta_get_current() == 2
assert playlist_tab._meta_get_next() == 1
assert playlist_tab._meta_get_notes() == [3]
playlist_tab._meta_clear_next()
assert playlist_tab._meta_get_played() == []
assert playlist_tab._meta_get_current() == 2
assert playlist_tab._meta_get_next() is None
assert playlist_tab._meta_get_notes() == [3]
playlist_tab._meta_clear_current()
assert playlist_tab._meta_get_played() == []
assert playlist_tab._meta_get_current() is None
assert playlist_tab._meta_get_next() is None
assert playlist_tab._meta_get_notes() == [3]
def test_clear_next(qtbot, session):
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
playlist_tab._meta_set_next(1)
assert playlist_tab._meta_get_next() == 1
playlist_tab.clear_next()
assert playlist_tab._meta_get_next() is None
def test_get_selected_row(qtbot, session):
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
window = Window()
window.show()
qtbot.addWidget(playlist_tab)
qtbot.wait_for_window_shown(playlist_tab)
row0_item0 = playlist_tab.item(0, 0)
assert row0_item0 is not None
rect = playlist_tab.visualItemRect(row0_item0)
qtbot.mouseClick(
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
)