Compare commits

..

No commits in common. "3e2293195a7b1da902b7c5c6717dc0f9e37d1330" and "d7c64141f271c3839fad13bd2364170f82aa24c3" have entirely different histories.

5 changed files with 69 additions and 100 deletions

View File

@ -148,35 +148,6 @@ def get_relative_date(
return f"{weeks} {weeks_str}, {days} {days_str} ago" 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( def leading_silence(
audio_segment: AudioSegment, audio_segment: AudioSegment,
silence_threshold: int = Config.DBFS_SILENCE, silence_threshold: int = Config.DBFS_SILENCE,
@ -360,13 +331,27 @@ def open_in_audacity(path: str) -> bool:
return True return True
def set_track_metadata(track): def set_track_metadata(session, track):
"""Set/update track metadata in database""" """Set/update track metadata in database"""
metadata = get_file_metadata(track.path) t = get_tags(track.path)
audio = get_audio_segment(track.path)
for key in metadata: track.title = t["title"]
setattr(track, key, metadata[key]) 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()
def show_OK(parent: QMainWindow, title: str, msg: str) -> None: def show_OK(parent: QMainWindow, title: str, msg: str) -> None:

View File

@ -3,7 +3,7 @@
import logging import logging
import logging.handlers import logging.handlers
import os import os
import stackprinter # type: ignore import stackprinter # type: ignore
import sys import sys
import traceback import traceback
@ -44,7 +44,7 @@ stderr = logging.StreamHandler()
stderr.setLevel(Config.LOG_LEVEL_STDERR) stderr.setLevel(Config.LOG_LEVEL_STDERR)
# syslog # syslog
syslog = logging.handlers.SysLogHandler(address="/dev/log") syslog = logging.handlers.SysLogHandler(address='/dev/log')
syslog.setLevel(Config.LOG_LEVEL_SYSLOG) syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
# Filter # Filter
@ -56,11 +56,10 @@ syslog.addFilter(local_filter)
stderr.addFilter(local_filter) stderr.addFilter(local_filter)
stderr.addFilter(debug_filter) stderr.addFilter(debug_filter)
stderr_fmt = logging.Formatter( stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s',
"[%(asctime)s] %(leveltag)s: %(message)s", datefmt="%H:%M:%S" datefmt='%H:%M:%S')
)
syslog_fmt = logging.Formatter( 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) stderr.setFormatter(stderr_fmt)
syslog.setFormatter(syslog_fmt) syslog.setFormatter(syslog_fmt)
@ -70,17 +69,18 @@ log.addHandler(syslog)
def log_uncaught_exceptions(_ex_cls, ex, tb): def log_uncaught_exceptions(_ex_cls, ex, tb):
from helpers import send_mail from helpers import send_mail
print("\033[1;31;47m") 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("\033[1;37;40m")
print(stackprinter.format(ex, style="darkbg")) # print(stackprinter.format(ex, show_vals="all", add_summary=True,
# style="darkbg"))
if os.environ["MM_ENV"] == "PRODUCTION": if os.environ["MM_ENV"] == "PRODUCTION":
msg = stackprinter.format(ex) msg = stackprinter.format(ex)
send_mail( send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
Config.ERRORS_TO, Config.ERRORS_FROM, "Exception from musicmuster", msg "Exception from musicmuster", msg)
)
sys.excepthook = log_uncaught_exceptions sys.excepthook = log_uncaught_exceptions

View File

@ -360,9 +360,7 @@ class PlaylistRows(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
plr_rownum: Mapped[int] plr_rownum: Mapped[int]
note: Mapped[str] = mapped_column( note: Mapped[str] = mapped_column(String(2048), index=False, default="", nullable=False)
String(2048), index=False, default="", nullable=False
)
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id")) playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"))
playlist: Mapped[Playlists] = relationship(back_populates="rows") playlist: Mapped[Playlists] = relationship(back_populates="rows")
track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id"))
@ -370,9 +368,7 @@ class PlaylistRows(Base):
"Tracks", "Tracks",
back_populates="playlistrows", back_populates="playlistrows",
) )
played: Mapped[bool] = mapped_column( played: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False)
Boolean, nullable=False, index=False, default=False
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -440,9 +436,7 @@ class PlaylistRows(Base):
session.flush() session.flush()
@classmethod @classmethod
def deep_rows( def deep_rows(cls, session: scoped_session, playlist_id: int) -> Sequence["PlaylistRows"]:
cls, session: scoped_session, playlist_id: int
) -> Sequence["PlaylistRows"]:
""" """
Return a list of playlist rows that include full track and lastplayed data for Return a list of playlist rows that include full track and lastplayed data for
given playlist_id., Sequence given playlist_id., Sequence
@ -673,13 +667,13 @@ class Tracks(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(256), index=True) title: Mapped[str] = mapped_column(String(256), index=True)
artist: Mapped[str] = mapped_column(String(256), index=True) artist: Mapped[str] = mapped_column(String(256), index=True)
bitrate: Mapped[Optional[int]] = mapped_column(default=None)
duration: Mapped[int] = mapped_column(index=True) 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) 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)
playlistrows: Mapped[List[PlaylistRows]] = relationship( playlistrows: Mapped[List[PlaylistRows]] = relationship(
"PlaylistRows", back_populates="track" "PlaylistRows", back_populates="track"
) )
@ -700,31 +694,34 @@ class Tracks(Base):
self, self,
session: scoped_session, session: scoped_session,
path: str, path: str,
title: str, title: Optional[str] = None,
artist: str, artist: Optional[str] = None,
duration: int, duration: int = 0,
start_gap: int, start_gap: int = 0,
fade_at: int, fade_at: Optional[int] = None,
silence_at: int, silence_at: Optional[int] = None,
mtime: int, mtime: Optional[float] = None,
bitrate: int lastplayed: Optional[datetime] = None,
): ) -> None:
self.path = path self.path = path
self.title = title self.title = title
self.artist = artist self.artist = artist
self.bitrate = bitrate
self.duration = duration self.duration = duration
self.start_gap = start_gap self.start_gap = start_gap
self.fade_at = fade_at self.fade_at = fade_at
self.silence_at = silence_at self.silence_at = silence_at
self.mtime = mtime self.mtime = mtime
self.lastplayed = lastplayed
try: try:
session.add(self) session.add(self)
session.commit() session.commit()
except IntegrityError as error: except IntegrityError as error:
session.rollback() session.rollback()
log.error(f"Error ({error=}) importing track ({path=})") log.error(
f"Error importing track ({title=}, "
f"{title=}, {artist=}, {path=}, {error=})"
)
raise ValueError raise ValueError
@classmethod @classmethod

View File

@ -199,31 +199,27 @@ class ImportTrack(QObject):
importing = pyqtSignal(str) importing = pyqtSignal(str)
finished = pyqtSignal(PlaylistTab) finished = pyqtSignal(PlaylistTab)
def __init__(self, playlist: PlaylistTab, filenames: list, row: int) -> None: def __init__(self, playlist: PlaylistTab, filenames: list) -> None:
super().__init__() super().__init__()
self.filenames = filenames self.filenames = filenames
self.playlist = playlist self.playlist = playlist
self.row = row
def run(self): def run(self):
""" """
Create track objects from passed files and add to visible playlist Create track objects from passed files and add to visible playlist
""" """
target_row = self.row
with Session() as session: with Session() as session:
for fname in self.filenames: for fname in self.filenames:
self.importing.emit(f"Importing {basename(fname)}") self.importing.emit(f"Importing {basename(fname)}")
metadata = helpers.get_file_metadata(fname)
try: try:
track = Tracks(session, **metadata) track = Tracks(session, fname)
except Exception as e: except ValueError:
print(e) self.import_error.emit(basename(fname))
return continue
helpers.set_track_metadata(session, track)
helpers.normalise_track(track.path) helpers.normalise_track(track.path)
self.playlist.insert_track(session, track, target_row) self.playlist.insert_track(session, track)
# Insert next row under this one
target_row += 1
# We're importing potentially multiple tracks in a loop. # We're importing potentially multiple tracks in a loop.
# If there's an error adding the track to the Tracks # If there's an error adding the track to the Tracks
# table, the session will rollback, thus losing any # table, the session will rollback, thus losing any
@ -962,6 +958,7 @@ class Window(QMainWindow, Ui_MainWindow):
for fname in dlg.selectedFiles(): for fname in dlg.selectedFiles():
txt = "" txt = ""
tags = helpers.get_tags(fname) tags = helpers.get_tags(fname)
new_tracks.append(fname)
title = tags["title"] title = tags["title"]
artist = tags["artist"] artist = tags["artist"]
count = 0 count = 0
@ -987,16 +984,11 @@ class Window(QMainWindow, Ui_MainWindow):
QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel,
) )
if result == QMessageBox.StandardButton.Cancel: if result == QMessageBox.StandardButton.Cancel:
continue return
new_tracks.append(fname)
# Import in separate thread # Import in separate thread
self.import_thread = QThread() self.import_thread = QThread()
self.worker = ImportTrack( self.worker = ImportTrack(self.visible_playlist_tab(), new_tracks)
self.visible_playlist_tab(),
new_tracks,
self.visible_playlist_tab().get_new_row_number(),
)
self.worker.moveToThread(self.import_thread) self.worker.moveToThread(self.import_thread)
self.import_thread.started.connect(self.worker.run) self.import_thread.started.connect(self.worker.run)
self.worker.finished.connect(self.import_thread.quit) self.worker.finished.connect(self.import_thread.quit)
@ -1121,7 +1113,7 @@ class Window(QMainWindow, Ui_MainWindow):
visible_tab.save_playlist(session) visible_tab.save_playlist(session)
# Disable sort undo # Disable sort undo
self.sort_undo: List[int] = [] self.sort_undo = []
# Update destination playlist_tab if visible (if not visible, it # Update destination playlist_tab if visible (if not visible, it
# will be re-populated when it is opened) # will be re-populated when it is opened)
@ -2178,7 +2170,7 @@ if __name__ == "__main__":
"Exception from musicmuster", "Exception from musicmuster",
msg, msg,
) )
else:
print("\033[1;31;47mUnhandled exception starts") # print("\033[1;31;47mUnhandled exception starts")
print(stackprinter.format(exc, style="darkbg")) # stackprinter.show(style="darkbg")
print("Unhandled exception ends\033[1;37;40m") # print("Unhandled exception ends\033[1;37;40m")

View File

@ -642,7 +642,6 @@ class PlaylistTab(QTableWidget):
track: Tracks, track: Tracks,
note: str = "", note: str = "",
repaint: bool = True, repaint: bool = True,
target_row: Optional[int] = None,
) -> None: ) -> None:
""" """
Insert track into playlist tab. Insert track into playlist tab.
@ -661,10 +660,7 @@ class PlaylistTab(QTableWidget):
) )
return return
if target_row: row_number = self.get_new_row_number()
row_number = target_row
else:
row_number = self.get_new_row_number()
# Check to see whether track is already in playlist # Check to see whether track is already in playlist
existing_plr = PlaylistRows.get_track_plr(session, track.id, self.playlist_id) existing_plr = PlaylistRows.get_track_plr(session, track.id, self.playlist_id)
@ -1717,7 +1713,7 @@ class PlaylistTab(QTableWidget):
self._set_row_colour_unreadable(row_number) self._set_row_colour_unreadable(row_number)
else: else:
self._set_row_colour_default(row_number) self._set_row_colour_default(row_number)
set_track_metadata(track) set_track_metadata(session, track)
self._update_row_track_info(session, row_number, track) self._update_row_track_info(session, row_number, track)
else: else:
_ = self._set_row_track_id(row_number, 0) _ = self._set_row_track_id(row_number, 0)
@ -2327,8 +2323,7 @@ class PlaylistTab(QTableWidget):
sorted_rows.append((row, None)) sorted_rows.append((row, None))
# Sort the list # Sort the list
reverse = QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier sorted_rows.sort(key=lambda row: row[1])
sorted_rows.sort(reverse=reverse, key=lambda row: row[1])
if sort_column == LASTPLAYED: if sort_column == LASTPLAYED:
sorted_rows.reverse() sorted_rows.reverse()