From 3e2293195a7b1da902b7c5c6717dc0f9e37d1330 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Mon, 16 Oct 2023 19:44:51 +0100 Subject: [PATCH] Improve track creation in database Pass all arguments to Tracks.__init__ on track creation Smarten up metadata collecting Reformat code Reinstate stackprinter, but with more sensible settings (mostly defaults, oddly enough) --- app/helpers.py | 51 ++++++++++++++++++++++++++++++---------------- app/log.py | 22 ++++++++++---------- app/models.py | 49 +++++++++++++++++++++++--------------------- app/musicmuster.py | 38 ++++++++++++++++++++-------------- app/playlists.py | 8 ++++++-- 5 files changed, 99 insertions(+), 69 deletions(-) diff --git a/app/helpers.py b/app/helpers.py index 1f72d85..80115d9 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -148,6 +148,35 @@ def get_relative_date( return f"{weeks} {weeks_str}, {days} {days_str} ago" +def get_file_metadata(filepath: str) -> dict: + """Return track metadata""" + + # Get title, artist, bitrate, duration, path + metadata: Dict[str, str | int | float] = get_tags(filepath) + + metadata['mtime'] = os.path.getmtime(filepath) + + # Set start_gap, fade_at and silence_at + audio = get_audio_segment(filepath) + if not audio: + audio_values = dict( + start_gap=0, + fade_at=0, + silence_at=0 + ) + else: + audio_values = dict( + start_gap=leading_silence(audio), + fade_at=int(round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000), + silence_at=int( + round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 + ) + ) + metadata |= audio_values + + return metadata + + def leading_silence( audio_segment: AudioSegment, silence_threshold: int = Config.DBFS_SILENCE, @@ -331,27 +360,13 @@ def open_in_audacity(path: str) -> bool: return True -def set_track_metadata(session, track): +def set_track_metadata(track): """Set/update track metadata in database""" - t = get_tags(track.path) - audio = get_audio_segment(track.path) + metadata = get_file_metadata(track.path) - track.title = t["title"] - track.artist = t["artist"] - track.bitrate = t["bitrate"] - - if not audio: - return - track.duration = len(audio) - track.start_gap = leading_silence(audio) - track.fade_at = round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 - track.silence_at = ( - round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 - ) - track.mtime = os.path.getmtime(track.path) - - session.commit() + for key in metadata: + setattr(track, key, metadata[key]) def show_OK(parent: QMainWindow, title: str, msg: str) -> None: diff --git a/app/log.py b/app/log.py index f198ab6..0bbe460 100644 --- a/app/log.py +++ b/app/log.py @@ -3,7 +3,7 @@ import logging import logging.handlers import os -import stackprinter # type: ignore +import stackprinter # type: ignore import sys import traceback @@ -44,7 +44,7 @@ stderr = logging.StreamHandler() stderr.setLevel(Config.LOG_LEVEL_STDERR) # syslog -syslog = logging.handlers.SysLogHandler(address='/dev/log') +syslog = logging.handlers.SysLogHandler(address="/dev/log") syslog.setLevel(Config.LOG_LEVEL_SYSLOG) # Filter @@ -56,10 +56,11 @@ syslog.addFilter(local_filter) stderr.addFilter(local_filter) stderr.addFilter(debug_filter) -stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s', - datefmt='%H:%M:%S') +stderr_fmt = logging.Formatter( + "[%(asctime)s] %(leveltag)s: %(message)s", datefmt="%H:%M:%S" +) syslog_fmt = logging.Formatter( - '[%(name)s] %(module)s.%(funcName)s - %(leveltag)s: %(message)s' + "[%(name)s] %(module)s.%(funcName)s - %(leveltag)s: %(message)s" ) stderr.setFormatter(stderr_fmt) syslog.setFormatter(syslog_fmt) @@ -69,18 +70,17 @@ log.addHandler(syslog) def log_uncaught_exceptions(_ex_cls, ex, tb): - from helpers import send_mail print("\033[1;31;47m") - logging.critical(''.join(traceback.format_tb(tb))) + logging.critical("".join(traceback.format_tb(tb))) print("\033[1;37;40m") - # print(stackprinter.format(ex, show_vals="all", add_summary=True, - # style="darkbg")) + print(stackprinter.format(ex, style="darkbg")) if os.environ["MM_ENV"] == "PRODUCTION": msg = stackprinter.format(ex) - send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, - "Exception from musicmuster", msg) + send_mail( + Config.ERRORS_TO, Config.ERRORS_FROM, "Exception from musicmuster", msg + ) sys.excepthook = log_uncaught_exceptions diff --git a/app/models.py b/app/models.py index 1dc9dec..2522bc6 100644 --- a/app/models.py +++ b/app/models.py @@ -360,7 +360,9 @@ class PlaylistRows(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) plr_rownum: Mapped[int] - note: Mapped[str] = mapped_column(String(2048), index=False, default="", nullable=False) + note: Mapped[str] = mapped_column( + String(2048), index=False, default="", nullable=False + ) playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id")) playlist: Mapped[Playlists] = relationship(back_populates="rows") track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) @@ -368,7 +370,9 @@ class PlaylistRows(Base): "Tracks", back_populates="playlistrows", ) - played: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False) + played: Mapped[bool] = mapped_column( + Boolean, nullable=False, index=False, default=False + ) def __repr__(self) -> str: return ( @@ -436,7 +440,9 @@ class PlaylistRows(Base): session.flush() @classmethod - def deep_rows(cls, session: scoped_session, playlist_id: int) -> Sequence["PlaylistRows"]: + def deep_rows( + cls, session: scoped_session, playlist_id: int + ) -> Sequence["PlaylistRows"]: """ Return a list of playlist rows that include full track and lastplayed data for given playlist_id., Sequence @@ -667,13 +673,13 @@ class Tracks(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) title: Mapped[str] = mapped_column(String(256), index=True) artist: Mapped[str] = mapped_column(String(256), index=True) - duration: Mapped[int] = mapped_column(index=True) - start_gap: Mapped[int] = mapped_column(index=False) - fade_at: Mapped[int] = mapped_column(index=False) - silence_at: Mapped[int] = mapped_column(index=False) - path: Mapped[str] = mapped_column(String(2048), index=False, unique=True) - mtime: Mapped[float] = mapped_column(index=True) bitrate: Mapped[Optional[int]] = mapped_column(default=None) + duration: Mapped[int] = mapped_column(index=True) + fade_at: Mapped[int] = mapped_column(index=False) + mtime: Mapped[float] = mapped_column(index=True) + path: Mapped[str] = mapped_column(String(2048), index=False, unique=True) + silence_at: Mapped[int] = mapped_column(index=False) + start_gap: Mapped[int] = mapped_column(index=False) playlistrows: Mapped[List[PlaylistRows]] = relationship( "PlaylistRows", back_populates="track" ) @@ -694,34 +700,31 @@ class Tracks(Base): self, session: scoped_session, path: str, - title: Optional[str] = None, - artist: Optional[str] = None, - duration: int = 0, - start_gap: int = 0, - fade_at: Optional[int] = None, - silence_at: Optional[int] = None, - mtime: Optional[float] = None, - lastplayed: Optional[datetime] = None, - ) -> None: + title: str, + artist: str, + duration: int, + start_gap: int, + fade_at: int, + silence_at: int, + mtime: int, + bitrate: int + ): self.path = path self.title = title self.artist = artist + self.bitrate = bitrate self.duration = duration self.start_gap = start_gap self.fade_at = fade_at self.silence_at = silence_at self.mtime = mtime - self.lastplayed = lastplayed try: session.add(self) session.commit() except IntegrityError as error: session.rollback() - log.error( - f"Error importing track ({title=}, " - f"{title=}, {artist=}, {path=}, {error=})" - ) + log.error(f"Error ({error=}) importing track ({path=})") raise ValueError @classmethod diff --git a/app/musicmuster.py b/app/musicmuster.py index bd461cc..d0e0b3a 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -199,27 +199,31 @@ class ImportTrack(QObject): importing = pyqtSignal(str) finished = pyqtSignal(PlaylistTab) - def __init__(self, playlist: PlaylistTab, filenames: list) -> None: + def __init__(self, playlist: PlaylistTab, filenames: list, row: int) -> None: super().__init__() self.filenames = filenames self.playlist = playlist + self.row = row def run(self): """ Create track objects from passed files and add to visible playlist """ + target_row = self.row with Session() as session: for fname in self.filenames: self.importing.emit(f"Importing {basename(fname)}") + metadata = helpers.get_file_metadata(fname) try: - track = Tracks(session, fname) - except ValueError: - self.import_error.emit(basename(fname)) - continue - helpers.set_track_metadata(session, track) + track = Tracks(session, **metadata) + except Exception as e: + print(e) + return helpers.normalise_track(track.path) - self.playlist.insert_track(session, track) + self.playlist.insert_track(session, track, target_row) + # Insert next row under this one + target_row += 1 # We're importing potentially multiple tracks in a loop. # If there's an error adding the track to the Tracks # table, the session will rollback, thus losing any @@ -958,7 +962,6 @@ class Window(QMainWindow, Ui_MainWindow): for fname in dlg.selectedFiles(): txt = "" tags = helpers.get_tags(fname) - new_tracks.append(fname) title = tags["title"] artist = tags["artist"] count = 0 @@ -984,11 +987,16 @@ class Window(QMainWindow, Ui_MainWindow): QMessageBox.StandardButton.Cancel, ) if result == QMessageBox.StandardButton.Cancel: - return + continue + new_tracks.append(fname) # Import in separate thread self.import_thread = QThread() - self.worker = ImportTrack(self.visible_playlist_tab(), new_tracks) + self.worker = ImportTrack( + self.visible_playlist_tab(), + new_tracks, + self.visible_playlist_tab().get_new_row_number(), + ) self.worker.moveToThread(self.import_thread) self.import_thread.started.connect(self.worker.run) self.worker.finished.connect(self.import_thread.quit) @@ -1113,7 +1121,7 @@ class Window(QMainWindow, Ui_MainWindow): visible_tab.save_playlist(session) # Disable sort undo - self.sort_undo = [] + self.sort_undo: List[int] = [] # Update destination playlist_tab if visible (if not visible, it # will be re-populated when it is opened) @@ -2170,7 +2178,7 @@ if __name__ == "__main__": "Exception from musicmuster", msg, ) - -# print("\033[1;31;47mUnhandled exception starts") -# stackprinter.show(style="darkbg") -# print("Unhandled exception ends\033[1;37;40m") + else: + print("\033[1;31;47mUnhandled exception starts") + print(stackprinter.format(exc, style="darkbg")) + print("Unhandled exception ends\033[1;37;40m") diff --git a/app/playlists.py b/app/playlists.py index e90eea4..c75039f 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -642,6 +642,7 @@ class PlaylistTab(QTableWidget): track: Tracks, note: str = "", repaint: bool = True, + target_row: Optional[int] = None, ) -> None: """ Insert track into playlist tab. @@ -660,7 +661,10 @@ class PlaylistTab(QTableWidget): ) return - row_number = self.get_new_row_number() + if target_row: + row_number = target_row + else: + row_number = self.get_new_row_number() # Check to see whether track is already in playlist existing_plr = PlaylistRows.get_track_plr(session, track.id, self.playlist_id) @@ -1713,7 +1717,7 @@ class PlaylistTab(QTableWidget): self._set_row_colour_unreadable(row_number) else: self._set_row_colour_default(row_number) - set_track_metadata(session, track) + set_track_metadata(track) self._update_row_track_info(session, row_number, track) else: _ = self._set_row_track_id(row_number, 0)