diff --git a/app/config.py b/app/config.py index 9bfe10f..2c65adb 100644 --- a/app/config.py +++ b/app/config.py @@ -6,7 +6,12 @@ from typing import List, Optional class Config(object): AUDACITY_COMMAND = "/usr/bin/audacity" AUDIO_SEGMENT_CHUNK_SIZE = 10 + BITRATE_LOW_THRESHOLD = 192 + BITRATE_OK_THRESHOLD = 300 CHECK_AUDACITY_AT_STARTUP = True + COLOUR_BITRATE_LOW = "#ffcdd2" + COLOUR_BITRATE_MEDIUM = "#ffeb6f" + COLOUR_BITRATE_OK = "#dcedc8" COLOUR_CURRENT_HEADER = "#d4edda" COLOUR_CURRENT_PLAYLIST = "#7eca8f" COLOUR_CURRENT_TAB = "#248f24" @@ -24,6 +29,7 @@ class Config(object): COLOUR_WARNING_TIMER = "#ffc107" COLUMN_NAME_ARTIST = "Artist" COLUMN_NAME_AUTOPLAY = "A" + COLUMN_NAME_BITRATE = "bps" COLUMN_NAME_END_TIME = "End" COLUMN_NAME_LAST_PLAYED = "Last played" COLUMN_NAME_LEADING_SILENCE = "Gap" diff --git a/app/helpers.py b/app/helpers.py index 298934d..7a7f437 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -86,6 +86,7 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]: return dict( title=tag.title, artist=tag.artist, + bitrate=round(tag.bitrate), duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000), path=path ) diff --git a/app/models.py b/app/models.py index 118ff21..359151e 100644 --- a/app/models.py +++ b/app/models.py @@ -510,6 +510,7 @@ class Tracks(Base): silence_at = Column(Integer, index=False) path = Column(String(2048), index=False, nullable=False, unique=True) mtime = Column(Float, index=True) + bitrate = Column(Integer, nullable=True, default=None) playlistrows = relationship("PlaylistRows", back_populates="track") playlists = association_proxy("playlistrows", "playlist") playdates = relationship("Playdates", back_populates="track") @@ -546,11 +547,11 @@ class Tracks(Base): session.add(self) session.commit() - @staticmethod - def get_all_paths(session) -> List[str]: - """Return a list of paths of all tracks""" + @classmethod + def get_all(cls, session) -> List["Tracks"]: + """Return a list of all tracks""" - return session.execute(select(Tracks.path)).scalars().all() + return session.execute(select(cls)).scalars().all() @classmethod def get_or_create(cls, session: Session, path: str) -> "Tracks": diff --git a/app/musicmuster.py b/app/musicmuster.py index 27daeb2..0ba1a3b 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -41,7 +41,7 @@ from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from config import Config from ui.main_window_ui import Ui_MainWindow # type: ignore -from utilities import create_track_from_file, check_db +from utilities import create_track_from_file, check_db, update_bitrates class TrackData: @@ -1134,6 +1134,9 @@ if __name__ == "__main__": p = argparse.ArgumentParser() # Only allow at most one option to be specified group = p.add_mutually_exclusive_group() + group.add_argument('-b', '--bitrates', + action="store_true", dest="update_bitrates", + default=False, help="Update bitrates in database") group.add_argument('-c', '--check-database', action="store_true", dest="check_db", default=False, help="Check and report on database") @@ -1144,6 +1147,10 @@ if __name__ == "__main__": log.debug("Updating database") with Session() as session: check_db(session) + elif args.update_bitrates: + log.debug("Update bitrates") + with Session() as session: + update_bitrates(session) else: # Normal run try: diff --git a/app/playlists.py b/app/playlists.py index d639e46..b731629 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -77,7 +77,8 @@ columns["duration"] = Column(idx=4, heading=Config.COLUMN_NAME_LENGTH) columns["start_time"] = Column(idx=5, heading=Config.COLUMN_NAME_START_TIME) columns["end_time"] = Column(idx=6, heading=Config.COLUMN_NAME_END_TIME) columns["lastplayed"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED) -columns["row_notes"] = Column(idx=8, heading=Config.COLUMN_NAME_NOTES) +columns["bitrate"] = Column(idx=8, heading=Config.COLUMN_NAME_BITRATE) +columns["row_notes"] = Column(idx=9, heading=Config.COLUMN_NAME_NOTES) class NoSelectDelegate(QStyledItemDelegate): @@ -95,6 +96,7 @@ class NoSelectDelegate(QStyledItemDelegate): return QPlainTextEdit(parent) return super().createEditor(parent, option, index) + class PlaylistTab(QTableWidget): # Qt.UserRoles ROW_FLAGS = Qt.UserRole @@ -582,6 +584,13 @@ class PlaylistTab(QTableWidget): end_item = QTableWidgetItem() self.setItem(row, columns['end_time'].idx, end_item) + if row_data.track.bitrate: + bitrate = str(row_data.track.bitrate) + else: + bitrate = "" + bitrate_item = QTableWidgetItem(bitrate) + self.setItem(row, columns['bitrate'].idx, bitrate_item) + # As we have track info, any notes should be contained in # the notes column notes_item = QTableWidgetItem(row_data.note) @@ -995,6 +1004,17 @@ class PlaylistTab(QTableWidget): # Ensure content is visible by wrapping cells self.resizeRowToContents(row) + # Highlight low bitrates + if track.bitrate: + if track.bitrate < Config.BITRATE_LOW_THRESHOLD: + cell_colour = Config.COLOUR_BITRATE_LOW + elif track.bitrate < Config.BITRATE_OK_THRESHOLD: + cell_colour = Config.COLOUR_BITRATE_MEDIUM + else: + cell_colour = Config.COLOUR_BITRATE_OK + brush = QBrush(QColor(cell_colour)) + self.item(row, columns['bitrate'].idx).setBackground(brush) + # Render playing track if row == current_row: # Set start time @@ -1421,6 +1441,7 @@ class PlaylistTab(QTableWidget): f"Artist: {track.artist}\n" f"Track ID: {track.id}\n" f"Track duration: {ms_to_mmss(track.duration)}\n" + f"Track bitrate: {track.bitrate}\n" f"Track fade at: {ms_to_mmss(track.fade_at)}\n" f"Track silence at: {ms_to_mmss(track.silence_at)}" "\n\n" diff --git a/app/utilities.py b/app/utilities.py index 50c00f6..4a54700 100755 --- a/app/utilities.py +++ b/app/utilities.py @@ -43,6 +43,7 @@ def create_track_from_file(session, path, normalise=None, tags=None): track.silence_at = round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 track.mtime = os.path.getmtime(path) + track.bitrate = t['bitrate'] session.commit() if normalise or normalise is None and Config.NORMALISE_ON_IMPORT: @@ -107,7 +108,7 @@ def check_db(session): Check all paths in database exist """ - db_paths = set(Tracks.get_all_paths(session)) + db_paths = set([a.path for a in Tracks.get_all(session)]) os_paths_list = [] for root, dirs, files in os.walk(Config.ROOT): @@ -162,6 +163,19 @@ def check_db(session): print("There were more paths than listed that were not found") +def update_bitrates(session): + """ + Update bitrates on all tracks in database + """ + + for track in Tracks.get_all(session): + try: + t = get_tags(track.path) + track.bitrate = t["bitrate"] + except FileNotFoundError: + continue + + # # Spike # # # # # Manage tracks listed in database but where path is invalid diff --git a/migrations/versions/ed3100326c38_add_column_for_bitrate_in_tracks.py b/migrations/versions/ed3100326c38_add_column_for_bitrate_in_tracks.py new file mode 100644 index 0000000..e2a51e3 --- /dev/null +++ b/migrations/versions/ed3100326c38_add_column_for_bitrate_in_tracks.py @@ -0,0 +1,28 @@ +"""Add column for bitrate in Tracks + +Revision ID: ed3100326c38 +Revises: fe2e127b3332 +Create Date: 2022-08-22 16:16:42.181848 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ed3100326c38' +down_revision = 'fe2e127b3332' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('tracks', sa.Column('bitrate', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('tracks', 'bitrate') + # ### end Alembic commands ###