diff --git a/app/models.py b/app/models.py index fc240aa..1a12a83 100644 --- a/app/models.py +++ b/app/models.py @@ -15,14 +15,17 @@ from sqlalchemy import ( delete, func, select, + text, update, ) -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError, ProgrammingError from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm import joinedload from sqlalchemy.orm.session import Session +from sqlalchemy.engine.row import RowMapping # App imports +from classes import ApplicationError from config import Config from dbmanager import DatabaseManager import dbtables @@ -38,6 +41,17 @@ if "unittest" in sys.modules and "sqlite" not in DATABASE_URL: db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db +def run_sql(session: Session, sql: str) -> Sequence[RowMapping]: + """ + Run a sql string and return results + """ + + try: + return session.execute(text(sql)).mappings().all() + except ProgrammingError as e: + raise ApplicationError(e) + + # Database classes class NoteColours(dbtables.NoteColoursTable): def __init__( diff --git a/app/musicmuster.py b/app/musicmuster.py index 8e5acac..2c7ab27 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -61,7 +61,7 @@ from classes import ( from config import Config from dialogs import TrackSelectDialog from file_importer import FileImporter -from helpers import file_is_unreadable +from helpers import ask_yes_no, file_is_unreadable, ms_to_mmss, show_OK from log import log from models import ( db, @@ -83,7 +83,6 @@ from ui.main_window_playlist_ui import Ui_PlaylistSection # type: ignore from ui.main_window_footer_ui import Ui_FooterSection # type: ignore from utilities import check_db, update_bitrates -import helpers class DownloadCSV(QDialog): @@ -869,8 +868,8 @@ class Window(QMainWindow): # Don't allow window to close when a track is playing if track_sequence.current and track_sequence.current.is_playing(): event.ignore() - helpers.show_warning( - self, "Track playing", "Can't close application while track is playing" + self.show_warning( + "Track playing", "Can't close application while track is playing" ) else: with db.Session() as session: @@ -927,7 +926,7 @@ class Window(QMainWindow): current_track_playlist_id = track_sequence.current.playlist_id if current_track_playlist_id: if closing_tab_playlist_id == current_track_playlist_id: - helpers.show_OK( + show_OK( "Current track", "Can't close current track playlist", self ) return False @@ -937,7 +936,7 @@ class Window(QMainWindow): next_track_playlist_id = track_sequence.next.playlist_id if next_track_playlist_id: if closing_tab_playlist_id == next_track_playlist_id: - helpers.show_OK( + show_OK( "Next track", "Can't close next track playlist", self ) return False @@ -1053,7 +1052,7 @@ class Window(QMainWindow): playlist_id = self.current.playlist_id playlist = session.get(Playlists, playlist_id) if playlist: - if helpers.ask_yes_no( + if ask_yes_no( "Delete playlist", f"Delete playlist '{playlist.name}': " "Are you sure?", ): @@ -1323,7 +1322,7 @@ class Window(QMainWindow): self.playlist_section.tabPlaylist.setCurrentIndex(idx) elif action == "Delete": - if helpers.ask_yes_no( + if ask_yes_no( "Delete template", f"Delete template '{playlist.name}': " "Are you sure?", ): @@ -1478,14 +1477,17 @@ class Window(QMainWindow): def open_querylist(self) -> None: """Open existing querylist""" - with db.Session() as session: - dlg = QueryDialog(session) - if dlg.exec(): - new_row_number = self.current_row_or_end() - for track_id in dlg.selected_tracks: - self.current.base_model.insert_row(new_row_number, track_id) - else: - return # User cancelled + try: + with db.Session() as session: + dlg = QueryDialog(session) + if dlg.exec(): + new_row_number = self.current_row_or_end() + for track_id in dlg.selected_tracks: + self.current.base_model.insert_row(new_row_number, track_id) + else: + return # User cancelled + except ApplicationError as e: + self.show_warning("Query error", f"Your query gave an error:\n\n{e}") def open_songfacts_browser(self, title: str) -> None: """Search Songfacts for title""" @@ -1759,7 +1761,7 @@ class Window(QMainWindow): msg = "Hit return to play next track now" else: msg = "Press tab to select Yes and hit return to play next track" - if not helpers.ask_yes_no( + if not ask_yes_no( "Play next track", msg, default_yes=default_yes, @@ -1829,12 +1831,12 @@ class Window(QMainWindow): template_name = dlg.textValue() if template_name not in template_names: break - helpers.show_warning( - self, "Duplicate template", "Template name already in use" + self.show_warning( + "Duplicate template", "Template name already in use" ) Playlists.save_as_template(session, self.current.playlist_id, template_name) session.commit() - helpers.show_OK("Template", "Template saved", self) + show_OK("Template", "Template saved", self) def search_playlist(self) -> None: """Show text box to search playlist""" @@ -1980,8 +1982,7 @@ class Window(QMainWindow): if Playlists.name_is_available(session, proposed_name): return proposed_name else: - helpers.show_warning( - self, + self.show_warning( "Name in use", f"There's already a playlist called '{proposed_name}'", ) @@ -2084,16 +2085,16 @@ class Window(QMainWindow): if track_sequence.current and track_sequence.current.is_playing(): # Elapsed time self.header_section.label_elapsed_timer.setText( - helpers.ms_to_mmss(track_sequence.current.time_playing()) + ms_to_mmss(track_sequence.current.time_playing()) + " / " - + helpers.ms_to_mmss(track_sequence.current.duration) + + ms_to_mmss(track_sequence.current.duration) ) # Time to fade time_to_fade = track_sequence.current.time_to_fade() time_to_silence = track_sequence.current.time_to_silence() self.footer_section.label_fade_timer.setText( - helpers.ms_to_mmss(time_to_fade) + ms_to_mmss(time_to_fade) ) # If silent in the next 5 seconds, put warning colour on @@ -2132,7 +2133,7 @@ class Window(QMainWindow): self.footer_section.frame_fade.setStyleSheet("") self.footer_section.label_silent_timer.setText( - helpers.ms_to_mmss(time_to_silence) + ms_to_mmss(time_to_silence) ) def update_headers(self) -> None: diff --git a/app/querylistmodel.py b/app/querylistmodel.py index 83b19eb..31e14fa 100644 --- a/app/querylistmodel.py +++ b/app/querylistmodel.py @@ -26,6 +26,7 @@ from sqlalchemy.orm.session import Session # App imports from classes import ( + ApplicationError, QueryCol, ) from config import Config @@ -33,9 +34,10 @@ from helpers import ( file_is_unreadable, get_relative_date, ms_to_mmss, + show_warning, ) from log import log -from models import db, Playdates +from models import db, Playdates, run_sql from music_manager import RowAndTrack @@ -220,33 +222,31 @@ class QuerylistModel(QAbstractTableModel): Populate self.querylist_rows """ - # TODO: Move the SQLAlchemy parts to models later, but for now as proof - # of concept we'll keep it here. - - from sqlalchemy import text - # Clear any exsiting rows self.querylist_rows = {} row = 0 - results = self.session.execute(text(self.sql)).mappings().all() - for result in results: - if hasattr(result, "lastplayed"): - lastplayed = result["lastplayed"] - else: - lastplayed = None - queryrow = QueryRow( - artist=result["artist"], - bitrate=result["bitrate"], - duration=result["duration"], - lastplayed=lastplayed, - path=result["path"], - title=result["title"], - track_id=result["id"], - ) + try: + results = run_sql(self.session, self.sql) + for result in results: + if hasattr(result, "lastplayed"): + lastplayed = result["lastplayed"] + else: + lastplayed = None + queryrow = QueryRow( + artist=result["artist"], + bitrate=result["bitrate"], + duration=result["duration"], + lastplayed=lastplayed, + path=result["path"], + title=result["title"], + track_id=result["id"], + ) - self.querylist_rows[row] = queryrow - row += 1 + self.querylist_rows[row] = queryrow + row += 1 + except ApplicationError as e: + show_warning(None, "Query error", f"Error loading query data ({e})") def rowCount(self, index: QModelIndex = QModelIndex()) -> int: """Standard function for view"""