From afc27c988df0665a3db3d84f4fd87911d36e2704 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 12 Aug 2022 11:57:34 +0100 Subject: [PATCH] Move info tabs to below playlist --- app/infotabs.py | 49 ++ app/musicmuster.py | 57 +- app/playlists.py | 32 +- app/ui/main_window.ui | 51 +- app/ui/main_window_ui.py | 14 +- ¡ | 1192 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 1318 insertions(+), 77 deletions(-) create mode 100644 app/infotabs.py create mode 100644 ¡ diff --git a/app/infotabs.py b/app/infotabs.py new file mode 100644 index 0000000..04bbc07 --- /dev/null +++ b/app/infotabs.py @@ -0,0 +1,49 @@ +import urllib.parse + +from datetime import datetime +from typing import Dict, Optional +from PyQt5.QtCore import QUrl +from PyQt5.QtWebEngineWidgets import QWebEngineView +from PyQt5.QtWidgets import QTabWidget +from config import Config + + +class InfoTabs(QTabWidget): + """ + Class to manage info tabs + """ + + def __init__(self, parent=None) -> None: + super().__init__(parent) + + # Dictionary to record when tabs were last updated (so we can + # re-use the oldest one later) + self.last_update: Dict[QWebEngineView, datetime] = {} + + def open_tab(self, title: str) -> None: + """ + Open passed URL. Create new tab if we're below the maximum + number otherwise reuse oldest content tab. + """ + + short_title = title[:Config.INFO_TAB_TITLE_LENGTH] + + if self.count() < Config.MAX_INFO_TABS: + # Create a new tab + widget = QWebEngineView() + widget.setZoomFactor(Config.WEB_ZOOM_FACTOR) + tab_index = self.addTab(widget, short_title) + + else: + # Reuse oldest widget + widget = min(self.last_update, key=self.last_update.get) + tab_index = self.indexOf(widget) + self.setTabText(tab_index, short_title) + + txt = urllib.parse.quote_plus(title) + url = Config.INFO_TAB_URL % txt + widget.setUrl(QUrl(url)) + self.last_update[widget] = datetime.now() + + # Show newly updated tab + self.setCurrentIndex(tab_index) diff --git a/app/musicmuster.py b/app/musicmuster.py index 747e1cd..92d09dd 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -5,7 +5,6 @@ from log import log # import psutil import sys # import threading -# import urllib.parse # import webbrowser # # @@ -15,7 +14,6 @@ import sys # from PyQt5.QtCore import QDate, QEvent, QProcess, Qt, QTime, QTimer, QUrl from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor -# from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWidgets import ( QApplication, QDialog, @@ -32,7 +30,6 @@ from dbconfig import engine, Session # import helpers # import music # -# from config import Config from models import ( Base, # Playdates, @@ -78,7 +75,6 @@ class Window(QMainWindow, Ui_MainWindow): # self.music: music.Music = music.Music() self.current_track: Optional[TrackData] = None self.current_track_playlist_tab: Optional[PlaylistTab] = None - self.info_tabs: Optional[Dict[str, QWebView]] = {} self.next_track: Optional[TrackData] = None self.next_track_playlist_tab: Optional[PlaylistTab] = None self.previous_track: Optional[TrackData] = None @@ -92,6 +88,7 @@ class Window(QMainWindow, Ui_MainWindow): # self.txtSearch.setHidden(True) # self.hide_played_tracks = False # + self.splitter.setSizes([200, 200]) self.visible_playlist_tab: Callable[[], PlaylistTab] = \ self.tabPlaylist.currentWidget # @@ -557,52 +554,6 @@ class Window(QMainWindow, Ui_MainWindow): destination_visible_playlist_tab.populate( session, dlg.playlist.id) # -# def open_info_tabs(self) -> None: -# """ -# Ensure we have info tabs for next and current track titles -# """ -# -# title_list: List[str] = [] -# -# if self.previous_track: -# title_list.append(self.previous_track.title) -# if self.current_track: -# title_list.append(self.current_track.title) -# if self.next_track: -# title_list.append(self.next_track.title) -# -# for title in title_list: -# if title in self.info_tabs.keys(): -# # We already have a tab for this track -# continue -# if len(self.info_tabs) >= Config.MAX_log.info_TABS: -# # Find an unneeded info tab -# try: -# old_title = list( -# set(self.info_tabs.keys()) - set(title_list) -# )[0] -# except IndexError: -# log.debug( -# f"ensure_info_tabs({title_list}): unable to add " -# f"{title=}" -# ) -# return -# # Assign redundant widget a new title -# widget = self.info_tabs[title] = self.info_tabs[old_title] -# idx = self.tabPlaylist.indexOf(widget) -# self.tabPlaylist.setTabText( -# idx, title[:Config.log.info_TAB_TITLE_LENGTH]) -# del self.info_tabs[old_title] -# else: -# # Create a new tab for this title -# widget = self.info_tabs[title] = QWebView() -# widget.setZoomFactor(Config.WEB_ZOOM_FACTOR) -# self.tabPlaylist.addTab( -# widget, title[:Config.log.info_TAB_TITLE_LENGTH]) -# txt = urllib.parse.quote_plus(title) -# url = Config.log.info_TAB_URL % txt -# widget.setUrl(QUrl(url)) -# # def play_next(self) -> None: # """ # Play next track. @@ -783,7 +734,7 @@ class Window(QMainWindow, Ui_MainWindow): # title = self.current_track.title # if title: # txt = urllib.parse.quote_plus(title) -# url = Config.log.info_TAB_URL % txt +# url = Config.TAB_URL % txt # webbrowser.open(url, new=2) # # def stop(self) -> None: @@ -879,8 +830,8 @@ class Window(QMainWindow, Ui_MainWindow): self.update_headers() # Populate 'info' tabs - self.open_info_tabs() -# + self.tabInfolist.open_tab(track.title) + # def tick(self) -> None: # """ # Carry out clock tick actions. diff --git a/app/playlists.py b/app/playlists.py index e5377f5..1427d89 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -4,6 +4,7 @@ from typing import List, Optional from PyQt5 import QtCore from PyQt5.QtCore import Qt from PyQt5.QtGui import ( + QBrush, QColor, QFont, QDropEvent @@ -364,12 +365,12 @@ class PlaylistTab(QTableWidget): # playlist.close(session) # # event.accept() -# -# def clear_next(self, session) -> None: -# """Clear next track""" -# -# self._meta_clear_next() -# self.update_display(session) + + def clear_next(self, session) -> None: + """Clear next track marker""" + + self._meta_clear_next() + self.update_display(session) # # def create_note(self) -> None: # """ @@ -1044,6 +1045,9 @@ class PlaylistTab(QTableWidget): continue # This is a track row other than next or current + # Reset colour in case it was current/next + self._set_row_colour(row, None) + if row in played: # Played today, so update last played column last_playedtime = track.lastplayed @@ -1277,7 +1281,7 @@ class PlaylistTab(QTableWidget): # Fix up row numbers left in this playlist PlaylistRows.fixup_rownumbers(session, self.playlist_id) - #Remove selected rows from display + # Remove selected rows from display self.remove_selected_rows() def _drop_on(self, event): @@ -1852,14 +1856,22 @@ class PlaylistTab(QTableWidget): if self.item(row, j): self.item(row, j).setFont(boldfont) - def _set_row_colour(self, row: int, colour: QColor) -> None: - """Set row background colour""" + def _set_row_colour(self, row: int, + colour: Optional[QColor] = None) -> None: + """ + Set or reset row background colour + """ j: int + if colour: + brush = QBrush(colour) + else: + brush = QBrush() + for j in range(1, self.columnCount()): if self.item(row, j): - self.item(row, j).setBackground(colour) + self.item(row, j).setBackground(brush) # # def _set_row_content(self, row: int, object_id: int) -> None: # """Set content associated with this row""" diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 97bc222..0417f05 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -282,19 +282,38 @@ border: 1px solid rgb(85, 87, 83); - - - -1 - - - false - - - true - - - true + + + Qt::Vertical + + + -1 + + + false + + + true + + + true + + + + + -1 + + + false + + + true + + + true + + @@ -1022,6 +1041,14 @@ border: 1px solid rgb(85, 87, 83); + + + InfoTabs + QTabWidget +
infotabs
+ 1 +
+
diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index 8424f10..c62e097 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -142,12 +142,20 @@ class Ui_MainWindow(object): self.frame_4.setFrameShadow(QtWidgets.QFrame.Raised) self.frame_4.setObjectName("frame_4") self.gridLayout_4.addWidget(self.frame_4, 1, 0, 1, 1) - self.tabPlaylist = QtWidgets.QTabWidget(self.centralwidget) + self.splitter = QtWidgets.QSplitter(self.centralwidget) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.tabPlaylist = QtWidgets.QTabWidget(self.splitter) self.tabPlaylist.setDocumentMode(False) self.tabPlaylist.setTabsClosable(True) self.tabPlaylist.setMovable(True) self.tabPlaylist.setObjectName("tabPlaylist") - self.gridLayout_4.addWidget(self.tabPlaylist, 2, 0, 1, 1) + self.tabInfolist = InfoTabs(self.splitter) + self.tabInfolist.setDocumentMode(False) + self.tabInfolist.setTabsClosable(True) + self.tabInfolist.setMovable(True) + self.tabInfolist.setObjectName("tabInfolist") + self.gridLayout_4.addWidget(self.splitter, 2, 0, 1, 1) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.frame = QtWidgets.QFrame(self.centralwidget) @@ -473,6 +481,7 @@ class Ui_MainWindow(object): self.retranslateUi(MainWindow) self.tabPlaylist.setCurrentIndex(-1) + self.tabInfolist.setCurrentIndex(-1) self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(MainWindow) @@ -550,4 +559,5 @@ class Ui_MainWindow(object): self.actionSearch.setShortcut(_translate("MainWindow", "/")) self.actionInsert_section_header.setText(_translate("MainWindow", "Insert §ion header...")) self.actionRemove.setText(_translate("MainWindow", "&Remove track")) +from infotabs import InfoTabs import icons_rc diff --git a/¡ b/¡ new file mode 100644 index 0000000..81916a7 --- /dev/null +++ b/¡ @@ -0,0 +1,1192 @@ +#!/usr/bin/env python + +from log import log +# import argparse +# import psutil +import sys +# import threading +# import urllib.parse +# import webbrowser +# +# +# from datetime import datetime, timedelta +# from typing import Callable, Dict, List, Optional, Tuple +# +# from PyQt5.QtCore import QDate, QEvent, QProcess, Qt, QTime, QTimer, QUrl +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QColor +# from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView +from PyQt5.QtWidgets import ( + QApplication, + QDialog, + # QFileDialog, + # QInputDialog, + QLabel, + # QLineEdit, + QListWidgetItem, + QMainWindow, + # QMessageBox, +) +# +from dbconfig import engine, Session +# import helpers +# import music +# +# from config import Config +from models import ( + Base, + # Playdates, + PlaylistRows, + Playlists, + Settings, + Tracks +) +from playlists import PlaylistTab +from sqlalchemy.orm.exc import DetachedInstanceError +# from ui.dlg_search_database_ui import Ui_Dialog +from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist +# from ui.downloadcsv_ui import Ui_DateSelect +from config import Config +from ui.main_window_ui import Ui_MainWindow +# from utilities import create_track_from_file, update_db +# +# +# log = logging.getLogger(Config.LOG_NAME) +class TrackData: + def __init__(self, track): + self.id = track.id + self.title = track.title + self.artist = track.artist + self.duration = track.duration + self.start_gap = track.start_gap + self.fade_at = track.fade_at + self.silence_at = track.silence_at + self.path = track.path + self.mtime = track.mtime + + +# class InfoTab(QWebView): +# """Subclass QWebView to show info about tracks""" +# +# def __init__(self, parent=None) -> None: +# super().__init__(parent) +# self.title = None + +class Window(QMainWindow, Ui_MainWindow): + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.setupUi(self) + +# self.timer: QTimer = QTimer() +# self.even_tick: bool = True +# self.playing: bool = False +# self.disable_play_next_controls() +# +# self.music: music.Music = music.Music() + self.current_track: Optional[TrackData] = None + self.current_track_playlist_tab: Optional[PlaylistTab] = None + self.info_tabs: Optional[Dict[str, QWebView]] = {} + self.next_track: Optional[TrackData] = None + self.next_track_playlist_tab: Optional[PlaylistTab] = None + self.previous_track: Optional[TrackData] = None + self.previous_track_position: Optional[int] = None + +# self.set_main_window_size() + self.lblSumPlaytime = QLabel("") + self.statusbar.addPermanentWidget(self.lblSumPlaytime) +# self.txtSearch = QLineEdit() +# self.statusbar.addWidget(self.txtSearch) +# self.txtSearch.setHidden(True) +# self.hide_played_tracks = False +# + self.splitter.setStretchSizes[200,200] + self.visible_playlist_tab: Callable[[], PlaylistTab] = \ + self.tabPlaylist.currentWidget +# + self._load_last_playlists() +# self.enable_play_next_controls() +# self.check_audacity() +# self.timer.start(Config.TIMER_MS) + self.connect_signals_slots() +# +# def set_main_window_size(self) -> None: +# """Set size of window from database""" +# +# with Session() as session: +# record = Settings.get_int_settings(session, "mainwindow_x") +# x = record.f_int or 1 +# record = Settings.get_int_settings(session, "mainwindow_y") +# y = record.f_int or 1 +# record = Settings.get_int_settings(session, "mainwindow_width") +# width = record.f_int or 1599 +# record = Settings.get_int_settings(session, "mainwindow_height") +# height = record.f_int or 981 +# self.setGeometry(x, y, width, height) +# return +# +# @staticmethod +# def print_current_database(): +# with Session() as session: +# db = session.bind.engine.url.database +# print(f"{db=}") +# +# @staticmethod +# def check_audacity() -> None: +# """Offer to run Audacity if not running""" +# +# if not Config.CHECK_AUDACITY_AT_STARTUP: +# return +# +# if "audacity" in [i.name() for i in psutil.process_iter()]: +# return +# +# if helpers.ask_yes_no("Audacity not running", "Start Audacity?"): +# QProcess.startDetached(Config.AUDACITY_COMMAND, []) + + def clear_selection(self) -> None: + """ Clear selected row""" + + if self.visible_playlist_tab(): + self.visible_playlist_tab().clear_selection() +# +# def closeEvent(self, event: QEvent) -> None: +# """Don't allow window to close when a track is playing""" +# +# if self.music.playing(): +# log.debug("closeEvent() ignored as music is playing") +# event.ignore() +# helpers.show_warning( +# "Track playing", +# "Can't close application while track is playing") +# else: +# log.debug("closeEvent() accepted") +# +# with Session() as session: +# record = Settings.get_int_settings( +# session, "mainwindow_height") +# if record.f_int != self.height(): +# record.update(session, {'f_int': self.height()}) +# +# record = Settings.get_int_settings(session, "mainwindow_width") +# if record.f_int != self.width(): +# record.update(session, {'f_int': self.width()}) +# +# record = Settings.get_int_settings(session, "mainwindow_x") +# if record.f_int != self.x(): +# record.update(session, {'f_int': self.x()}) +# +# record = Settings.get_int_settings(session, "mainwindow_y") +# if record.f_int != self.y(): +# record.update(session, {'f_int': self.y()}) +# +# # Find a playlist tab (as opposed to an info tab) and +# # save column widths +# if self.current_track_playlist_tab: +# self.current_track_playlist_tab.close() +# elif self.next_track_playlist_tab: +# self.next_track_playlist_tab.close() +# +# event.accept() + + def connect_signals_slots(self) -> None: +# 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.actionDownload_CSV_of_played_tracks.triggered.connect( +# self.download_played_tracks) +# self.actionEnable_controls.triggered.connect( +# self.enable_play_next_controls) +# self.actionExport_playlist.triggered.connect(self.export_playlist_tab) +# self.actionImport.triggered.connect(self.import_track) +# self.actionFade.triggered.connect(self.fade) +# self.actionMoveSelected.triggered.connect(self.move_selected) +# self.actionNewPlaylist.triggered.connect(self.create_playlist) +# self.actionOpenPlaylist.triggered.connect(self.open_playlist) +# self.actionPlay_next.triggered.connect(self.play_next) +# self.actionSearch.triggered.connect(self.search_playlist) +# self.actionSearch_database.triggered.connect(self.search_database) +# self.actionSelect_next_track.triggered.connect(self.select_next_row) +# self.actionSelect_played_tracks.triggered.connect(self.select_played) +# self.actionSelect_previous_track.triggered.connect( +# self.select_previous_row) +# self.actionSelect_unplayed_tracks.triggered.connect( +# self.select_unplayed) +# self.actionSetNext.triggered.connect( +# lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) +# self.actionSkip_next.triggered.connect(self.play_next) +# self.actionStop.triggered.connect(self.stop) +# # self.btnAddNote.clicked.connect(self.create_note) +# # self.btnDatabase.clicked.connect(self.search_database) +# self.btnDrop3db.clicked.connect(self.drop3db) +# self.btnHidePlayed.clicked.connect(self.hide_played) +# self.btnFade.clicked.connect(self.fade) +# # self.btnPlay.clicked.connect(self.play_next) +# # self.btnSetNext.clicked.connect( +# # lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) +# # self.btnSongInfo.clicked.connect(self.song_info_search) +# self.btnStop.clicked.connect(self.stop) +# self.tabPlaylist.tabCloseRequested.connect(self.close_tab) +# self.txtSearch.returnPressed.connect(self.search_playlist_return) +# self.txtSearch.textChanged.connect(self.search_playlist_update) +# +# self.timer.timeout.connect(self.tick) +# +# def create_playlist(self) -> None: +# """Create new playlist""" +# +# dlg = QInputDialog(self) +# dlg.setInputMode(QInputDialog.TextInput) +# dlg.setLabelText("Playlist name:") +# dlg.resize(500, 100) +# ok = dlg.exec() +# if ok: +# with Session() as session: +# playlist = Playlists(session, dlg.textValue()) +# self.create_playlist_tab(session, playlist) +# +# def close_playlist_tab(self) -> None: +# """Close active playlist tab""" +# +# self.close_tab(self.tabPlaylist.currentIndex()) +# +# def close_tab(self, index: int) -> None: +# """ +# Close tab unless it holds the curren or next track +# """ +# +# if hasattr(self.tabPlaylist.widget(index), 'playlist_id'): +# if self.tabPlaylist.widget(index) == ( +# self.current_track_playlist_tab): +# self.statusbar.showMessage( +# "Can't close current track playlist", 5000) +# return +# if self.tabPlaylist.widget(index) == self.next_track_playlist_tab: +# self.statusbar.showMessage( +# "Can't close next track playlist", 5000) +# return +# # It's OK to close this playlist so remove from open playlist list +# self.tabPlaylist.widget(index).close() +# +# # Close regardless of tab type +# self.tabPlaylist.removeTab(index) +# +# def create_note(self) -> None: +# """Call playlist to create note""" +# +# try: +# self.visible_playlist_tab().create_note() +# except AttributeError: +# # Just return if there's no visible playlist tab +# return + + def create_playlist_tab(self, session: Session, + playlist: Playlists) -> None: + """ + Take the passed playlist database object, create a playlist tab and + add tab to display. + """ + + playlist_tab: PlaylistTab = PlaylistTab( + musicmuster=self, session=session, playlist_id=playlist.id) + idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name) + self.tabPlaylist.setCurrentIndex(idx) +# +# def disable_play_next_controls(self) -> None: +# """ +# Disable "play next" keyboard controls +# """ +# +# log.debug("disable_play_next_controls()") +# self.actionPlay_next.setEnabled(False) +# self.statusbar.showMessage("Play controls: Disabled", 0) +# +# def download_played_tracks(self) -> None: +# """Download a CSV of played tracks""" +# +# dlg = DownloadCSV(self) +# if dlg.exec(): +# start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime() +# # Get output filename +# pathspec: Tuple[str, str] = QFileDialog.getSaveFileName( +# self, 'Save CSV of tracks played', +# directory="/tmp/playlist.csv", +# filter="CSV files (*.csv)" +# ) +# if not pathspec: +# return +# +# path: str = pathspec[0] +# if not path.endswith(".csv"): +# path += ".csv" +# +# with open(path, "w") as f: +# with Session() as session: +# for playdate in Playdates.played_after(session, start_dt): +# f.write( +# f"{playdate.track.artist},{playdate.track.title}\n" +# ) +# +# def drop3db(self) -> None: +# """Drop music level by 3db if button checked""" +# +# if self.btnDrop3db.isChecked(): +# self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False) +# else: +# self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False) +# +# def enable_play_next_controls(self) -> None: +# """ +# Enable "play next" keyboard controls +# """ +# +# log.debug("enable_play_next_controls()") +# self.actionPlay_next.setEnabled(True) +# self.statusbar.showMessage("Play controls: Enabled", 0) +# +# def end_of_track_actions(self) -> None: +# """ +# Clean up after track played +# +# Actions required: +# - Set flag to say we're not playing a track +# - Reset current track +# - Tell playlist_tab track has finished +# - Reset current playlist_tab +# - Reset clocks +# - Update headers +# - Enable controls +# """ +# +# # Set flag to say we're not playing a track so that tick() +# # doesn't see player=None and kick off end-of-track actions +# self.playing = False +# +# # Reset current track +# if self.current_track: +# self.previous_track = self.current_track +# self.current_track = None +# +# # Tell playlist_tab track has finished and +# # reset current playlist_tab +# if self.current_track_playlist_tab: +# self.current_track_playlist_tab.play_stopped() +# self.current_track_playlist_tab = None +# +# # Reset clocks +# self.frame_fade.setStyleSheet("") +# self.frame_silent.setStyleSheet("") +# self.label_elapsed_timer.setText("00:00") +# self.label_end_timer.setText("00:00") +# self.label_fade_length.setText("0:00") +# self.label_fade_timer.setText("00:00") +# self.label_silent_timer.setText("00:00") +# self.label_track_length.setText("0:00") +# self.label_start_time.setText("00:00:00") +# self.label_end_time.setText("00:00:00") +# +# # Update headers +# self.update_headers() +# +# # Enable controls +# self.enable_play_next_controls() +# +# def export_playlist_tab(self) -> None: +# """Export the current playlist to an m3u file""" +# +# if not self.visible_playlist_tab(): +# return +# +# with Session() as session: +# playlist = Playlists.get_by_id( +# session, self.visible_playlist_tab().playlist_id) +# # Get output filename +# pathspec: Tuple[str, str] = QFileDialog.getSaveFileName( +# self, 'Save Playlist', +# directory=f"{playlist.name}.m3u", +# filter="M3U files (*.m3u);;All files (*.*)" +# ) +# if not pathspec: +# return +# +# path: str = 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 playlist.tracks.items(): +# f.write( +# "#EXTINF:" +# f"{int(track.duration / 1000)}," +# f"{track.title} - " +# f"{track.artist}" +# "\n" +# f"{track.path}" +# "\n" +# ) +# +# def fade(self) -> None: +# """Fade currently playing track""" +# +# log.debug("musicmuster:fade()", True) +# +# self.stop_playing(fade=True) +# +# def hide_played(self): +# """Toggle hide played tracks""" +# +# if self.hide_played_tracks: +# self.hide_played_tracks = False +# self.btnHidePlayed.setText("Hide played") +# else: +# self.hide_played_tracks = True +# self.btnHidePlayed.setText("Show played") +# if self.current_track_playlist_tab: +# with Session() as session: +# self.current_track_playlist_tab.update_display(session) +# +# def import_track(self) -> None: +# """Import track file""" +# +# dlg = QFileDialog() +# dlg.setFileMode(QFileDialog.ExistingFiles) +# dlg.setViewMode(QFileDialog.Detail) +# dlg.setDirectory(Config.IMPORT_DESTINATION) +# dlg.setNameFilter("Music files (*.flac *.mp3)") +# +# if dlg.exec_(): +# with Session() as session: +# txt: str = "" +# new_tracks = [] +# for fname in dlg.selectedFiles(): +# tags = helpers.get_tags(fname) +# new_tracks.append((fname, tags)) +# title = tags['title'] +# artist = tags['artist'] +# possible_matches = Tracks.search_titles(session, title) +# if possible_matches: +# txt += 'Similar to new track ' +# txt += f'"{title}" by "{artist} ({fname})":\n\n' +# for track in possible_matches: +# txt += f' "{track.title}" by {track.artist}' +# txt += f' ({track.path})\n' +# txt += "\n" +# # Check whether to proceed if there were potential matches +# if txt: +# txt += "Proceed with import?" +# result = QMessageBox.question(self, +# "Possible duplicates", +# txt, +# QMessageBox.Ok, +# QMessageBox.Cancel +# ) +# if result == QMessageBox.Cancel: +# return +# +# # Import in separate thread +# thread = threading.Thread(target=self._import_tracks, +# args=(new_tracks,)) +# thread.start() +# +# def _import_tracks(self, tracks: list): +# """ +# Import passed files. Don't use parent session as that may be invalid +# by the time we need it. +# """ +# +# with Session() as session: +# for (fname, tags) in tracks: +# track = create_track_from_file(session, fname, tags=tags) +# # 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) + + def _load_last_playlists(self) -> None: + """Load the playlists that were open when the last session closed""" + + with Session() as session: + for playlist in Playlists.get_open(session): + self.create_playlist_tab(session, playlist) + playlist.mark_open(session) + + def move_selected(self) -> None: + """ + Move selected rows to another playlist + + Actions required: + - identify destination playlist + - update playlist for the rows in the database + - remove them from the display + - update destination playlist display if loaded + """ + + # Identify destination playlist + with Session() as session: + visible_tab = self.visible_playlist_tab() + source_playlist = visible_tab.playlist_id + playlists = [] + for playlist in Playlists.get_all(session): + if playlist.id == source_playlist: + continue + else: + playlists.append(playlist) + + # Get destination playlist id + dlg = SelectPlaylistDialog(self, playlists=playlists, + session=session) + dlg.exec() + if not dlg.playlist: + return + destination_playlist = dlg.playlist + + # Update playlist for the rows in the database + plr_ids = visible_tab.get_selected_playlistrow_ids() + PlaylistRows.move_to_playlist( + session, plr_ids, destination_playlist.id + ) + + # Remove moved rows from display + visible_tab.remove_selected_rows() + + # Update destination playlist_tab if visible (if not visible, it + # will be re-populated when it is opened) + destination_visible_playlist_tab = None + for tab in range(self.tabPlaylist.count()): + # Non-playlist tabs won't have a 'playlist_id' attribute + if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'): + continue + if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id: + destination_visible_playlist_tab = ( + self.tabPlaylist.widget(tab)) + break + if destination_visible_playlist_tab: + destination_visible_playlist_tab.populate( + session, dlg.playlist.id) + + def open_info_tabs(self) -> None: + """ + Ensure we have info tabs for next and current track titles + """ + + title_list: List[str] = [] + + if self.previous_track: + title_list.append(self.previous_track.title) + if self.current_track: + title_list.append(self.current_track.title) + if self.next_track: + title_list.append(self.next_track.title) + + for title in title_list: + if title in self.info_tabs.keys(): + # We already have a tab for this track + continue + if len(self.info_tabs) >= Config.MAX_log.info_TABS: + # Find an unneeded info tab + try: + old_title = list( + set(self.info_tabs.keys()) - set(title_list) + )[0] + except IndexError: + log.debug( + f"ensure_info_tabs({title_list}): unable to add " + f"{title=}" + ) + return + # Assign redundant widget a new title + widget = self.info_tabs[title] = self.info_tabs[old_title] + idx = self.tabPlaylist.indexOf(widget) + self.tabPlaylist.setTabText( + idx, title[:Config.log.info_TAB_TITLE_LENGTH]) + del self.info_tabs[old_title] + else: + # Create a new tab for this title + widget = self.info_tabs[title] = QWebView() + widget.setZoomFactor(Config.WEB_ZOOM_FACTOR) + self.tabPlaylist.addTab( + widget, title[:Config.log.info_TAB_TITLE_LENGTH]) + txt = urllib.parse.quote_plus(title) + url = Config.log.info_TAB_URL % txt + widget.setUrl(QUrl(url)) +# +# def play_next(self) -> None: +# """ +# Play next track. +# +# Actions required: +# - If there is no next track set, return. +# - If there's currently a track playing, fade it. +# - Move next track to current track. +# - Update record of current track playlist_tab +# - If current track on different playlist_tab to last, reset +# last track playlist_tab colour +# - Set current track playlist_tab colour +# - Restore volume if -3dB active +# - Play (new) current track. +# - Tell database to record it as played +# - Tell playlist track is now playing +# - Disable play next controls +# - Update headers +# - Update clocks +# """ +# +# log.debug( +# "musicmuster.play_next(), " +# f"next_track={self.next_track.title if self.next_track else None} " +# "current_track=" +# f"{self.current_track.title if self.current_track else None}", +# True +# ) +# +# # If there is no next track set, return. +# if not self.next_track: +# log.debug("musicmuster.play_next(): no next track selected", True) +# return +# +# with Session() as session: +# # If there's currently a track playing, fade it. +# self.stop_playing(fade=True) +# +# # Move next track to current track. +# self.current_track = self.next_track +# self.next_track = None +# +# # If current track on different playlist_tab to last, reset +# # last track playlist_tab colour +# # Set current track playlist_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_NORMAL_TAB)) +# +# # Update record of current track playlist_tab +# self.current_track_playlist_tab = self.next_track_playlist_tab +# self.next_track_playlist_tab = None +# +# # Set current track playlist_tab colour +# self.set_tab_colour(self.current_track_playlist_tab, +# QColor(Config.COLOUR_CURRENT_TAB)) +# +# # Restore volume if -3dB active +# if self.btnDrop3db.isChecked(): +# self.btnDrop3db.setChecked(False) +# +# # Play (new) current track +# start_at = datetime.now() +# self.music.play(self.current_track.path) +# +# # Tell database to record it as played +# Playdates(session, self.current_track.id) +# +# # Set last_played date +# Tracks.update_lastplayed(session, self.current_track.id) +# +# # Tell playlist track is now playing +# self.current_track_playlist_tab.play_started(session) +# +# # Disable play next controls +# self.disable_play_next_controls() +# +# # Update headers +# self.update_headers() +# +# # Update clocks +# self.label_track_length.setText( +# helpers.ms_to_mmss(self.current_track.duration) +# ) +# fade_at = self.current_track.fade_at +# silence_at = self.current_track.silence_at +# length = self.current_track.duration +# self.label_fade_length.setText( +# helpers.ms_to_mmss(silence_at - fade_at)) +# self.label_start_time.setText( +# start_at.strftime(Config.TRACK_TIME_FORMAT)) +# end_at = start_at + timedelta( +# milliseconds=self.current_track.duration) +# self.label_end_time.setText( +# end_at.strftime(Config.TRACK_TIME_FORMAT)) +# +# def search_database(self) -> None: +# """Show dialog box to select and cue track from database""" +# +# with Session() as session: +# dlg = DbDialog(self, session) +# dlg.exec() +# +# def search_playlist(self): +# """Show text box to search playlist""" +# +# self.disable_play_next_controls() +# self.txtSearch.setHidden(False) +# self.txtSearch.setFocus() +# +# def search_playlist_return(self): +# """Close off search box when return pressed""" +# +# self.txtSearch.setText("") +# self.txtSearch.setHidden(True) +# self.enable_play_next_controls() +# self.visible_playlist_tab().set_filter("") +# +# def search_playlist_update(self): +# """Update search when search string changes""" +# +# self.visible_playlist_tab().set_filter(self.txtSearch.text()) +# +# def open_playlist(self): +# with Session() as session: +# playlists = Playlists.get_closed(session) +# dlg = SelectPlaylistDialog(self, playlists=playlists, +# session=session) +# dlg.exec() +# playlist = dlg.playlist +# if playlist: +# playlist.mark_open(session) +# self.create_playlist_tab(session, playlist) +# +# def select_next_row(self) -> None: +# """Select next or first row in playlist""" +# +# self.visible_playlist_tab().select_next_row() +# +# def select_played(self) -> None: +# """Select all played tracks in playlist""" +# +# self.visible_playlist_tab().select_played_tracks() +# +# def select_previous_row(self) -> None: +# """Select previous or first row in playlist""" +# +# self.visible_playlist_tab().select_previous_row() +# +# def select_unplayed(self) -> None: +# """Select all unplayed tracks in playlist""" +# +# self.visible_playlist_tab().select_unplayed_tracks() + + def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None: + """ + Find the tab containing the widget and set the text colour + """ + + idx = self.tabPlaylist.indexOf(widget) + self.tabPlaylist.tabBar().setTabTextColor(idx, colour) +# +# def song_info_search(self) -> None: +# """ +# Open browser tab for Wikipedia, searching for +# the first that exists of: +# - selected track +# - next track +# - current track +# """ +# +# title: Optional[str] = self.visible_playlist_tab().get_selected_title() +# if not title: +# if self.next_track: +# title = self.next_track.title +# if not title: +# if self.current_track: +# title = self.current_track.title +# if title: +# txt = urllib.parse.quote_plus(title) +# url = Config.log.info_TAB_URL % txt +# webbrowser.open(url, new=2) +# +# def stop(self) -> None: +# """Stop playing immediately""" +# +# log.debug("musicmuster.stop()") +# +# self.stop_playing(fade=False) +# +# def stop_playing(self, fade=True) -> None: +# """ +# Stop playing current track +# +# Actions required: +# - Return if not playing +# - Stop/fade track +# - Reset playlist_tab colour +# - Run end-of-track actions +# """ +# +# log.debug(f"musicmuster.stop_playing({fade=})", True) +# +# # Return if not playing +# if not self.playing: +# log.debug("musicmuster.stop_playing(): not playing", True) +# return +# +# # Stop/fade track +# self.previous_track_position = self.music.get_position() +# if fade: +# log.debug("musicmuster.stop_playing(): fading music", True) +# self.music.fade() +# else: +# log.debug("musicmuster.stop_playing(): stopping music", True) +# self.music.stop() +# +# # Reset playlist_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)) +# else: +# self.set_tab_colour(self.current_track_playlist_tab, +# QColor(Config.COLOUR_NORMAL_TAB)) +# +# # Run end-of-track actions +# self.end_of_track_actions() + + def this_is_the_next_track(self, playlist_tab: PlaylistTab, + track: Tracks, session) -> None: + """ + This is notification from a playlist tab that it holds the next + track to be played. + + Actions required: + - Clear next track if on other tab + - Reset tab colour if on other tab + - Note next playlist tab + - Set next playlist_tab tab colour + - Note next track + - Update headers + - Populate ‘info’ tabs + + """ + + # Clear next track if on another tab + 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(session) + + # Reset tab colour if on other 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)) + + # Note next playlist tab + self.next_track_playlist_tab = playlist_tab + + # Set next playlist_tab tab colour 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)) + + # Note next track + self.next_track = TrackData(track) + + # Update headers + self.update_headers() + + # Populate 'info' tabs + self.open_info_tabs() +# +# def tick(self) -> None: +# """ +# Carry out clock tick actions. +# +# The Time of Day clock is updated every tick (500ms). +# +# All other timers are updated every second. As the timers have a +# one-second resolution, updating every 500ms can result in some +# timers updating and then, 500ms later, other timers updating. That +# looks odd. +# +# Actions required: +# - Update TOD clock +# - If track is playing, update track clocks time and colours +# - Else: run stop_track +# """ +# +# # Update TOD clock +# self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT)) +# +# self.even_tick = not self.even_tick +# if not self.even_tick: +# return +# +# # If track is playing, update track clocks time and colours +# if self.music.player and self.music.playing(): +# self.playing = True +# playtime: int = self.music.get_playtime() +# time_to_fade: int = (self.current_track.fade_at - playtime) +# time_to_silence: int = ( +# self.current_track.silence_at - playtime) +# time_to_end: int = (self.current_track.duration - playtime) +# +# # Elapsed time +# if time_to_end < 500: +# self.label_elapsed_timer.setText( +# helpers.ms_to_mmss(playtime) +# ) +# else: +# self.label_elapsed_timer.setText( +# helpers.ms_to_mmss(playtime) +# ) +# +# # Time to fade +# self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) +# +# # If silent in the next 5 seconds, put warning colour on +# # time to silence box and enable play controls +# if time_to_silence <= 5500: +# self.frame_silent.setStyleSheet( +# f"background: {Config.COLOUR_ENDING_TIMER}" +# ) +# self.enable_play_next_controls() +# # Set warning colour on time to silence box when fade starts +# elif time_to_fade <= 500: +# self.frame_silent.setStyleSheet( +# f"background: {Config.COLOUR_WARNING_TIMER}" +# ) +# # Five seconds before fade starts, set warning colour on +# # time to silence box and enable play controls +# elif time_to_fade <= 5500: +# self.frame_fade.setStyleSheet( +# f"background: {Config.COLOUR_WARNING_TIMER}" +# ) +# self.enable_play_next_controls() +# else: +# self.frame_silent.setStyleSheet("") +# self.frame_fade.setStyleSheet("") +# +# self.label_silent_timer.setText( +# helpers.ms_to_mmss(time_to_silence) +# ) +# +# # Time to end +# self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) +# +# else: +# if self.playing: +# self.stop_playing() + + def update_headers(self) -> None: + """ + Update last / current / next track headers + """ + + try: + self.hdrPreviousTrack.setText( + f"{self.previous_track.title} - {self.previous_track.artist}" + ) + except (AttributeError, DetachedInstanceError): + self.hdrPreviousTrack.setText("") + + try: + self.hdrCurrentTrack.setText( + f"{self.current_track.title} - {self.current_track.artist}" + ) + except (AttributeError, DetachedInstanceError): + self.hdrCurrentTrack.setText("") + + try: + self.hdrNextTrack.setText( + f"{self.next_track.title} - {self.next_track.artist}" + ) + except (AttributeError, DetachedInstanceError): + self.hdrNextTrack.setText("") +# +# +# class DbDialog(QDialog): +# """Select track from database""" +# +# def __init__(self, parent, session): # review +# super().__init__(parent) +# self.session = session +# self.ui = Ui_Dialog() +# self.ui.setupUi(self) +# self.ui.btnAdd.clicked.connect(self.add_selected) +# self.ui.btnAddClose.clicked.connect(self.add_selected_and_close) +# 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.title_artist_toggle) +# self.ui.searchString.textEdited.connect(self.chars_typed) +# +# record = Settings.get_int_settings(self.session, "dbdialog_width") +# width = record.f_int or 800 +# record = Settings.get_int_settings(self.session, "dbdialog_height") +# height = record.f_int or 600 +# self.resize(width, height) +# +# def __del__(self): # review +# record = Settings.get_int_settings(self.session, "dbdialog_height") +# if record.f_int != self.height(): +# record.update(self.session, {'f_int': self.height()}) +# +# record = Settings.get_int_settings(self.session, "dbdialog_width") +# if record.f_int != self.width(): +# record.update(self.session, {'f_int': self.width()}) +# +# def add_selected(self): # review +# if not self.ui.matchList.selectedItems(): +# return +# +# item = self.ui.matchList.currentItem() +# track = item.data(Qt.UserRole) +# self.add_track(track) +# +# def add_selected_and_close(self): # review +# self.add_selected() +# self.close() +# +# def title_artist_toggle(self): # review +# """ +# Handle switching between searching for artists and searching for +# titles +# """ +# +# # Logic is handled already in chars_typed(), so just call that. +# self.chars_typed(self.ui.searchString.text()) +# +# def chars_typed(self, s): # review +# if len(s) > 0: +# if self.ui.radioTitle.isChecked(): +# matches = Tracks.search_titles(self.session, s) +# else: +# matches = Tracks.search_artists(self.session, s) +# self.ui.matchList.clear() +# if matches: +# for track in matches: +# t = QListWidgetItem() +# t.setText( +# f"{track.title} - {track.artist} " +# f"[{helpers.ms_to_mmss(track.duration)}] " +# f"({helpers.get_relative_date(track.lastplayed)})" +# ) +# t.setData(Qt.UserRole, track) +# self.ui.matchList.addItem(t) +# +# def double_click(self, entry): # review +# 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): # review +# # Add to playlist on screen +# self.parent().visible_playlist_tab().insert_track( +# self.session, track) +# # Commit session to get correct row numbers if more tracks added +# self.session.commit() +# # Select search text to make it easier for next search +# self.select_searchtext() +# +# def select_searchtext(self): # review +# self.ui.searchString.selectAll() +# self.ui.searchString.setFocus() +# +# def selection_changed(self): # review +# if not self.ui.matchList.selectedItems(): +# return +# +# item = self.ui.matchList.currentItem() +# track = item.data(Qt.UserRole) +# self.ui.dbPath.setText(track.path) +# +# +# class DownloadCSV(QDialog): +# def __init__(self, parent=None): +# super().__init__(parent) +# +# self.ui = Ui_DateSelect() +# self.ui.setupUi(self) +# self.ui.dateTimeEdit.setDate(QDate.currentDate()) +# self.ui.dateTimeEdit.setTime(QTime(19, 59, 0)) +# self.ui.buttonBox.accepted.connect(self.accept) +# self.ui.buttonBox.rejected.connect(self.reject) + + +class SelectPlaylistDialog(QDialog): + def __init__(self, parent=None, playlists=None, session=None): + super().__init__(parent) + + if playlists is None: + return + self.ui = Ui_dlgSelectPlaylist() + self.ui.setupUi(self) + self.ui.lstPlaylists.itemDoubleClicked.connect(self.list_doubleclick) + self.ui.buttonBox.accepted.connect(self.open) + self.ui.buttonBox.rejected.connect(self.close) + self.session = session + self.playlist = None + self.plid = None + + record = Settings.get_int_settings( + self.session, "select_playlist_dialog_width") + width = record.f_int or 800 + record = Settings.get_int_settings( + self.session, "select_playlist_dialog_height") + height = record.f_int or 600 + self.resize(width, height) + + for playlist in playlists: + p = QListWidgetItem() + p.setText(playlist.name) + p.setData(Qt.UserRole, playlist) + self.ui.lstPlaylists.addItem(p) + + def __del__(self): # review + record = Settings.get_int_settings( + self.session, "select_playlist_dialog_height") + if record.f_int != self.height(): + record.update(self.session, {'f_int': self.height()}) + + record = Settings.get_int_settings( + self.session, "select_playlist_dialog_width") + if record.f_int != self.width(): + record.update(self.session, {'f_int': self.width()}) + + def list_doubleclick(self, entry): # review + self.playlist = entry.data(Qt.UserRole) + self.accept() + + def open(self): # review + if self.ui.lstPlaylists.selectedItems(): + item = self.ui.lstPlaylists.currentItem() + self.playlist = item.data(Qt.UserRole) + self.accept() + + +if __name__ == "__main__": + # p = argparse.ArgumentParser() + # # Only allow at most one option to be specified + # group = p.add_mutually_exclusive_group() + # group.add_argument('-u', '--update', + # action="store_true", dest="update", + # default=False, help="Update database") + # # group.add_argument('-f', '--full-update', + # # action="store_true", dest="full_update", + # # default=False, help="Update database") + # # group.add_argument('-i', '--import', dest="fname", help="Input file") + # args = p.parse_args() + # + # # Run as required + # if args.update: + # log.debug("Updating database") + # with Session() as session: + # update_db(session) + # # elif args.full_update: + # # log.debug("Full update of database") + # # with Session() as session: + # # full_update_db(session) + # else: + # # Normal run + try: + Base.metadata.create_all(engine) + app = QApplication(sys.argv) + win = Window() + win.show() + sys.exit(app.exec()) + except Exception: + msg = "Unhandled Exception caught by musicmuster.main()" + log.exception(msg, exc_info=True, stack_info=True)