Rebase dev onto v2_id
This commit is contained in:
parent
b92a0927f8
commit
cf58932fca
@ -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
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
497
app/playlists.py
497
app/playlists.py
@ -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.
|
||||
"""
|
||||
|
||||
@ -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
19
poetry.lock
generated
@ -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"},
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user