Compare commits
No commits in common. "3e2293195a7b1da902b7c5c6717dc0f9e37d1330" and "d7c64141f271c3839fad13bd2364170f82aa24c3" have entirely different histories.
3e2293195a
...
d7c64141f2
@ -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:
|
||||||
|
|||||||
22
app/log.py
22
app/log.py
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user