From 99409e86267f3b4c8d2f1b285bf3d9e77a3d3373 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 7 Aug 2022 20:20:56 +0100 Subject: [PATCH] Right-click menu mostly working Still to implement: - Move to playlist - Remove row --- app/config.py | 1 + app/helpers.py | 258 +++++++++++++++++++++++---------------------- app/models.py | 48 ++++----- app/musicmuster.py | 116 ++++++++++---------- app/playlists.py | 207 +++++++++++++++++++----------------- poetry.lock | 11 +- pyproject.toml | 1 + 7 files changed, 339 insertions(+), 303 deletions(-) diff --git a/app/config.py b/app/config.py index e5b8b5f..c32cc21 100644 --- a/app/config.py +++ b/app/config.py @@ -60,6 +60,7 @@ class Config(object): IMPORT_DESTINATION = os.path.join(ROOT, "Singles") SCROLL_TOP_MARGIN = 3 TESTMODE = True + TEXT_NO_TRACK_NO_NOTE = "[Section header]" TOD_TIME_FORMAT = "%H:%M:%S" TIMER_MS = 500 TRACK_TIME_FORMAT = "%H:%M:%S" diff --git a/app/helpers.py b/app/helpers.py index 8252a3a..080372a 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -1,11 +1,12 @@ import os -# import psutil -# -# from config import Config +import psutil + +from config import Config from datetime import datetime -# from pydub import AudioSegment +from pydub import AudioSegment # from PyQt5.QtWidgets import QMessageBox # from tinytag import TinyTag +from typing import Optional # from typing import Dict, Optional, Union # # @@ -15,35 +16,35 @@ from datetime import datetime # button_reply: bool = QMessageBox.question(None, title, question) # # return button_reply == QMessageBox.Yes -# -# -# def fade_point( -# audio_segment: AudioSegment, fade_threshold: int = 0, -# chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: -# """ -# Returns the millisecond/index of the point where the volume drops below -# the maximum and doesn't get louder again. -# audio_segment - the sdlg_search_database_uiegment to find silence in -# fade_threshold - the upper bound for how quiet is silent in dFBS -# chunk_size - chunk size for interating over the segment in ms -# """ -# -# assert chunk_size > 0 # to avoid infinite loop -# -# segment_length: int = audio_segment.duration_seconds * 1000 # ms -# trim_ms: int = segment_length - chunk_size -# max_vol: int = audio_segment.dBFS -# if fade_threshold == 0: -# fade_threshold = max_vol -# -# while ( -# audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold -# and trim_ms > 0): # noqa W503 -# trim_ms -= chunk_size -# -# # if there is no trailing silence, return lenght of track (it's less -# # the chunk_size, but for chunk_size = 10ms, this may be ignored) -# return int(trim_ms) + + +def fade_point( + audio_segment: AudioSegment, fade_threshold: float = 0.0, + chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: + """ + Returns the millisecond/index of the point where the volume drops below + the maximum and doesn't get louder again. + audio_segment - the sdlg_search_database_uiegment to find silence in + fade_threshold - the upper bound for how quiet is silent in dFBS + chunk_size - chunk size for interating over the segment in ms + """ + + assert chunk_size > 0 # to avoid infinite loop + + segment_length: int = audio_segment.duration_seconds * 1000 # ms + trim_ms = segment_length - chunk_size + max_vol = audio_segment.dBFS + if fade_threshold == 0: + fade_threshold = max_vol + + while ( + audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold + and trim_ms > 0): # noqa W503 + trim_ms -= chunk_size + + # if there is no trailing silence, return lenght of track (it's less + # the chunk_size, but for chunk_size = 10ms, this may be ignored) + return int(trim_ms) def file_is_readable(path: str, check_colon: bool = True) -> bool: @@ -60,16 +61,19 @@ def file_is_readable(path: str, check_colon: bool = True) -> bool: return True return False -# -# def get_audio_segment(path: str) -> Optional[AudioSegment]: -# try: -# if path.endswith('.mp3'): -# return AudioSegment.from_mp3(path) -# elif path.endswith('.flac'): -# return AudioSegment.from_file(path, "flac") -# except AttributeError: -# return None -# + + +def get_audio_segment(path: str) -> Optional[AudioSegment]: + try: + if path.endswith('.mp3'): + return AudioSegment.from_mp3(path) + elif path.endswith('.flac'): + return AudioSegment.from_file(path, "flac") + except AttributeError: + return None + + return None + # # def get_tags(path: str) -> Dict[str, Union[str, int]]: # """ @@ -126,32 +130,32 @@ def get_relative_date(past_date: datetime, reference_date: datetime = None) \ else: days_str = "days" return f"{weeks} {weeks_str}, {days} {days_str} ago" -# -# -# def leading_silence( -# audio_segment: AudioSegment, -# silence_threshold: int = Config.DBFS_SILENCE, -# chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: -# """ -# Returns the millisecond/index that the leading silence ends. -# audio_segment - the segment to find silence in -# silence_threshold - the upper bound for how quiet is silent in dFBS -# chunk_size - chunk size for interating over the segment in ms -# -# https://github.com/jiaaro/pydub/blob/master/pydub/silence.py -# """ -# -# trim_ms: int = 0 # ms -# assert chunk_size > 0 # to avoid infinite loop -# while ( -# audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504 -# silence_threshold and trim_ms < len(audio_segment)): -# trim_ms += chunk_size -# -# # if there is no end it should return the length of the segment -# return min(trim_ms, len(audio_segment)) -# -# + + +def leading_silence( + audio_segment: AudioSegment, + silence_threshold: int = Config.DBFS_SILENCE, + chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: + """ + Returns the millisecond/index that the leading silence ends. + audio_segment - the segment to find silence in + silence_threshold - the upper bound for how quiet is silent in dFBS + chunk_size - chunk size for interating over the segment in ms + + https://github.com/jiaaro/pydub/blob/master/pydub/silence.py + """ + + trim_ms: int = 0 # ms + assert chunk_size > 0 # to avoid infinite loop + while ( + audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504 + silence_threshold and trim_ms < len(audio_segment)): + trim_ms += chunk_size + + # if there is no end it should return the length of the segment + return min(trim_ms, len(audio_segment)) + + def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str: """Convert milliseconds to mm:ss""" @@ -177,66 +181,68 @@ def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str: seconds = 59.0 return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}" -# -# -# def open_in_audacity(path: str) -> Optional[bool]: -# """ -# Open passed file in Audacity -# -# Return True if apparently opened successfully, else False -# """ -# -# # Return if audacity not running -# if "audacity" not in [i.name() for i in psutil.process_iter()]: -# return False -# -# # Return if path not given -# if not path: -# return False -# -# to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid()) -# from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid()) -# eol: str = '\n' -# -# def send_command(command: str) -> None: -# """Send a single command.""" -# to_audacity.write(command + eol) -# to_audacity.flush() -# -# def get_response() -> str: -# """Return the command response.""" -# -# result: str = '' -# line: str = '' -# -# while True: -# result += line -# line = from_audacity.readline() -# if line == '\n' and len(result) > 0: -# break -# return result -# -# def do_command(command: str) -> str: -# """Send one command, and return the response.""" -# -# send_command(command) -# response = get_response() -# return response -# -# with open(to_pipe, 'w') as to_audacity, open( -# from_pipe, 'rt') as from_audacity: -# do_command(f'Import2: Filename="{path}"') + + +def open_in_audacity(path: str) -> bool: + """ + Open passed file in Audacity + + Return True if apparently opened successfully, else False + """ + + # Return if audacity not running + if "audacity" not in [i.name() for i in psutil.process_iter()]: + return False + + # Return if path not given + if not path: + return False + + to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid()) + from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid()) + eol: str = '\n' + + def send_command(command: str) -> None: + """Send a single command.""" + to_audacity.write(command + eol) + to_audacity.flush() + + def get_response() -> str: + """Return the command response.""" + + result: str = '' + line: str = '' + + while True: + result += line + line = from_audacity.readline() + if line == '\n' and len(result) > 0: + break + return result + + def do_command(command: str) -> str: + """Send one command, and return the response.""" + + send_command(command) + response = get_response() + return response + + with open(to_pipe, 'w') as to_audacity, open( + from_pipe, 'rt') as from_audacity: + do_command(f'Import2: Filename="{path}"') + + return True # # # def show_warning(title: str, msg: str) -> None: # """Display a warning to user""" # # QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel) -# -# -# def trailing_silence( -# audio_segment: AudioSegment, silence_threshold: int = -50, -# chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: -# """Return fade point from start in milliseconds""" -# -# return fade_point(audio_segment, silence_threshold, chunk_size) + + +def trailing_silence( + audio_segment: AudioSegment, silence_threshold: int = -50, + chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: + """Return fade point from start in milliseconds""" + + return fade_point(audio_segment, silence_threshold, chunk_size) diff --git a/app/models.py b/app/models.py index e91e27b..66b3cb2 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 # -# import os.path +import os.path import re # from dbconfig import Session @@ -36,13 +36,13 @@ from sqlalchemy.orm.exc import ( NoResultFound ) # -# from config import Config -# from helpers import ( -# fade_point, -# get_audio_segment, -# leading_silence, -# trailing_silence, -# ) +from config import Config +from helpers import ( + fade_point, + get_audio_segment, + leading_silence, + trailing_silence, +) from log import log # Base = declarative_base() @@ -669,22 +669,22 @@ class Tracks(Base): # except NoResultFound: # log.error(f"get_track({track_id}): not found") # return None -# -# def rescan(self, session: Session) -> None: -# """ -# Update audio metadata for passed track. -# """ -# -# audio: AudioSegment = get_audio_segment(self.path) -# self.duration = len(audio) -# self.fade_at = round(fade_point(audio) / 1000, -# Config.MILLISECOND_SIGFIGS) * 1000 -# self.mtime = os.path.getmtime(self.path) -# self.silence_at = round(trailing_silence(audio) / 1000, -# Config.MILLISECOND_SIGFIGS) * 1000 -# self.start_gap = leading_silence(audio) -# session.add(self) -# session.flush() + + def rescan(self, session: Session) -> None: + """ + Update audio metadata for passed track. + """ + + audio: AudioSegment = get_audio_segment(self.path) + self.duration = len(audio) + self.fade_at = round(fade_point(audio) / 1000, + Config.MILLISECOND_SIGFIGS) * 1000 + self.mtime = os.path.getmtime(self.path) + self.silence_at = round(trailing_silence(audio) / 1000, + Config.MILLISECOND_SIGFIGS) * 1000 + self.start_gap = leading_silence(audio) + session.add(self) + session.flush() # # @staticmethod # def remove_by_path(session: Session, path: str) -> None: diff --git a/app/musicmuster.py b/app/musicmuster.py index 362d045..e511731 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -37,7 +37,7 @@ from models import ( # Playdates, Playlists, # Settings, - # Tracks + Tracks ) from playlists import PlaylistTab # from sqlalchemy.orm.exc import DetachedInstanceError @@ -501,10 +501,13 @@ class Window(QMainWindow, Ui_MainWindow): for playlist in Playlists.get_open(session): self.create_playlist_tab(session, playlist) playlist.mark_open(session) -# -# def move_selected(self) -> None: -# """Move selected rows to another playlist""" -# + + def move_selected(self) -> None: + """Move selected rows to another playlist""" + + # ***KAE + pass + # with Session() as session: # visible_tab = self.visible_playlist_tab() # visible_tab_id = visible_tab.playlist_id @@ -819,56 +822,59 @@ class Window(QMainWindow, Ui_MainWindow): # # # Run end-of-track actions # self.end_of_track_actions() -# -# def this_is_the_next_track(self, playlist_tab: PlaylistTab, -# track: Tracks, session) -> None: -# """ -# This is notification from a playlist tab that it holds the next -# track to be played. -# -# Actions required: -# - Clear next track if on other tab -# - Reset tab colour if on other tab -# - Note next playlist tab -# - Set next playlist_tab tab colour -# - Note next track -# - Update headers -# - Populate ‘info’ tabs -# -# """ -# -# # Clear next track if on another tab -# if self.next_track_playlist_tab != playlist_tab: -# # We need to reset the ex-next-track playlist -# if self.next_track_playlist_tab: -# self.next_track_playlist_tab.clear_next(session) -# -# # Reset tab colour if on other tab -# if (self.next_track_playlist_tab != -# self.current_track_playlist_tab): -# self.set_tab_colour( -# self.next_track_playlist_tab, -# QColor(Config.COLOUR_NORMAL_TAB)) -# -# # Note next playlist tab -# self.next_track_playlist_tab = playlist_tab -# -# # Set next playlist_tab tab colour if it isn't the -# # currently-playing tab -# if (self.next_track_playlist_tab != -# self.current_track_playlist_tab): -# self.set_tab_colour( -# self.next_track_playlist_tab, -# QColor(Config.COLOUR_NEXT_TAB)) -# -# # Note next track -# self.next_track = TrackData(track) -# -# # Update headers -# self.update_headers() -# -# # Populate 'info' tabs -# self.open_info_tabs() + + def this_is_the_next_track(self, playlist_tab: PlaylistTab, + track: Tracks, session) -> None: + """ + This is notification from a playlist tab that it holds the next + track to be played. + + Actions required: + - Clear next track if on other tab + - Reset tab colour if on other tab + - Note next playlist tab + - Set next playlist_tab tab colour + - Note next track + - Update headers + - Populate ‘info’ tabs + + """ + + # ***kae + return + + # Clear next track if on another tab + if self.next_track_playlist_tab != playlist_tab: + # We need to reset the ex-next-track playlist + if self.next_track_playlist_tab: + self.next_track_playlist_tab.clear_next(session) + + # Reset tab colour if on other tab + if (self.next_track_playlist_tab != + self.current_track_playlist_tab): + self.set_tab_colour( + self.next_track_playlist_tab, + QColor(Config.COLOUR_NORMAL_TAB)) + + # Note next playlist tab + self.next_track_playlist_tab = playlist_tab + + # Set next playlist_tab tab colour if it isn't the + # currently-playing tab + if (self.next_track_playlist_tab != + self.current_track_playlist_tab): + self.set_tab_colour( + self.next_track_playlist_tab, + QColor(Config.COLOUR_NEXT_TAB)) + + # Note next track + self.next_track = TrackData(track) + + # Update headers + self.update_headers() + + # Populate 'info' tabs + self.open_info_tabs() # # def tick(self) -> None: # """ diff --git a/app/playlists.py b/app/playlists.py index d2d2141..f3a171b 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -15,9 +15,9 @@ from PyQt5.QtWidgets import ( # QInputDialog, # QLineEdit, QMainWindow, - # QMenu, + QMenu, # QStyledItemDelegate, - # QMessageBox, + QMessageBox, QTableWidget, QTableWidgetItem, ) @@ -25,14 +25,14 @@ from PyQt5.QtWidgets import ( import helpers # import os import re -# import subprocess -# import threading +import subprocess +import threading # from config import Config from datetime import datetime # , timedelta from helpers import ( get_relative_date, - # open_in_audacity + open_in_audacity ) from log import log from models import ( @@ -102,7 +102,7 @@ class PlaylistTab(QTableWidget): self.musicmuster: QMainWindow = musicmuster self.playlist_id: int = playlist_id -# self.menu: Optional[QMenu] = None + self.menu: Optional[QMenu] = None # self.current_track_start_time: Optional[datetime] = None # # # Don't select text on edit @@ -293,7 +293,7 @@ class PlaylistTab(QTableWidget): # Rescan act_rescan = self.menu.addAction("Rescan") act_rescan.triggered.connect( - lambda: self._rescan(track_id) + lambda: self._rescan(row_number, track_id) ) self.menu.addSeparator() @@ -557,7 +557,7 @@ class PlaylistTab(QTableWidget): # self._set_row_content(row, track.id) # # # Mark track if file is unreadable -# if not self._file_is_readable(track.path): +# if not helpers.file_is_readable(track.path): # self._set_unreadable_row(row) # # Scroll to new row # self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) @@ -1242,10 +1242,13 @@ class PlaylistTab(QTableWidget): # """Clear played status on row""" # # self._meta_clear_attribute(row, RowMeta.PLAYED) -# -# def _delete_rows(self) -> None: -# """Delete mutliple rows""" -# + + def _delete_rows(self) -> None: + """Delete mutliple rows""" + + # ***KAE + pass + # log.debug("playlist._delete_rows()") # # selected_rows: List[int] = sorted( @@ -1444,18 +1447,18 @@ class PlaylistTab(QTableWidget): # notes_rows: Set[int] = set(self._get_notes_rows()) # # return list(unplayed_rows - notes_rows) -# -# def _get_row_start_time(self, row: int) -> Optional[datetime]: -# try: -# if self.item(row, self.COL_START_TIME): -# return datetime.strptime(self.item( -# row, self.COL_START_TIME).text(), -# Config.NOTE_TIME_FORMAT -# ) -# else: -# return None -# except ValueError: -# return None + + def _get_row_start_time(self, row: int) -> Optional[datetime]: + try: + if self.item(row, columns['start_time'].idx): + return datetime.strptime(self.item( + row, columns['start_time'].idx).text(), + Config.NOTE_TIME_FORMAT + ) + else: + return None + except ValueError: + return None # # def _get_row_track_object(self, row: int, session: Session) \ # -> Optional[Tracks]: @@ -1578,15 +1581,15 @@ class PlaylistTab(QTableWidget): # new_metadata: int = self._meta_get(row) & ~(1 << attribute) # self.item(row, self.COL_USERDATA).setData( # self.ROW_FLAGS, new_metadata) -# -# def _meta_clear_next(self) -> None: -# """ -# Clear next row if there is one. -# """ -# -# next_row: Optional[int] = self._get_next_track_row() -# if next_row is not None: -# self._meta_clear_attribute(next_row, RowMeta.NEXT) + + def _meta_clear_next(self) -> None: + """ + Clear next row if there is one. + """ + + next_row: Optional[int] = self._get_next_track_row() + if next_row is not None: + self._meta_clear_attribute(next_row, RowMeta.NEXT) def _meta_get(self, row: int) -> int: """Return row metadata""" @@ -1637,20 +1640,20 @@ class PlaylistTab(QTableWidget): f"in rows: {', '.join([str(x) for x in matches])}" ) raise AttributeError(f"Multiple '{metadata}' metadata {matches}") -# -# def _meta_set_attribute(self, row: int, attribute: int) -> None: -# """Set row metadata""" -# -# if row is None: -# raise ValueError(f"_meta_set_attribute({row=}, {attribute=})") -# -# current_metadata: int = self._meta_get(row) -# if not current_metadata: -# new_metadata: int = (1 << attribute) -# else: -# new_metadata = self._meta_get(row) | (1 << attribute) -# self.item(row, self.COL_USERDATA).setData( -# self.ROW_FLAGS, new_metadata) + + def _meta_set_attribute(self, row: int, attribute: int) -> None: + """Set row metadata""" + + if row is None: + raise ValueError(f"_meta_set_attribute({row=}, {attribute=})") + + current_metadata: int = self._meta_get(row) + if not current_metadata: + new_metadata: int = (1 << attribute) + else: + new_metadata = self._meta_get(row) | (1 << attribute) + self.item(row, columns['userdata'].idx).setData( + self.ROW_FLAGS, new_metadata) def _mplayer_play(self, track_id: int) -> None: """Play track with mplayer""" @@ -1673,21 +1676,30 @@ class PlaylistTab(QTableWidget): """Remove track from row, making it a section header""" # Update playlist_rows record - plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) - plr.track_id = None - plr.save() + with Session() as session: + plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) + plr.track_id = None + # We can't have null text + if not plr.note: + plr.note = Config.TEXT_NO_TRACK_NO_NOTE + session.commit() - # Clear track text items - for i in range(2, len(columns) - 1): - self.item(row, i).setText("") - # Set note text in correct column for section head - self.item(row, 1).setText(plr.note) - # Remove row duration - self._set_row_duration(row, 0) - # And refresh display - self.update_display() + # Clear track text items + for i in range(2, len(columns)): + self.item(row, i).setText("") + # Set note text in correct column for section head + self.item(row, 1).setText(plr.note) + # Remove row duration + self._set_row_duration(row, 0) + # Remote track_id from row + self.item(row, columns['userdata'].idx).setData( + self.ROW_TRACK_ID, 0) + # Span the rows + self.setSpan(row, 1, 1, len(columns)) + # And refresh display + self.update_display(session) - def _rescan(self, track_id: int) -> None: + def _rescan(self, row: int, track_id: int) -> None: """Rescan track""" with Session() as session: @@ -1699,13 +1711,13 @@ class PlaylistTab(QTableWidget): ) return - track.rescan(session) - self._update_row(session, row, track) + track.rescan(session) + self._update_row(session, row, track) -# def _run_subprocess(self, args): -# """Run args in subprocess""" -# -# subprocess.call(args) + def _run_subprocess(self, args): + """Run args in subprocess""" + + subprocess.call(args) # # def _set_current_track_row(self, row: int) -> None: # """Mark this row as current track""" @@ -1796,7 +1808,7 @@ class PlaylistTab(QTableWidget): else: self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) - def _set_next(self, session: Session, row: int) -> None: + def _set_next(self, session: Session, row_number: int) -> None: """ Set passed row as next track to play. @@ -1810,21 +1822,23 @@ class PlaylistTab(QTableWidget): track_id = self._get_row_track_id(row_number) if not track_id: - log.error(f"playlists._set_next({row=}) has no track associated") + log.error( + f"playlists._set_next({row_number=}) has no track associated" + ) return track = session.get(Tracks, track_id) if not track: - log.error(f"playlists._set_next({row=}): Track not found") + log.error(f"playlists._set_next({row_number=}): Track not found") return # Check track is readable - if not self._file_is_readable(track.path): - self._set_unreadable_row(row) + if not helpers.file_is_readable(track.path): + self._set_unreadable_row(row_number) return None # Mark as next track - self._set_next_track_row(row) + self._set_next_track_row(row_number) # Update display self.update_display(session) @@ -1904,28 +1918,27 @@ class PlaylistTab(QTableWidget): # display_text = note_text + ' [' + duration + caveat + ']' # item = self.item(start_row, self.COL_TITLE) # item.setText(display_text) -# -# def _update_row(self, session, row: int, track: Tracks) -> None: -# """ -# Update the passed row with info from the passed track. -# """ -# -# log.debug(f"_update_row({row=}, {track=}") -# -# item_startgap: QTableWidgetItem = self.item(row, self.COL_MSS) -# item_startgap.setText(str(track.start_gap)) -# if track.start_gap >= 500: -# item_startgap.setBackground(QColor(Config.COLOUR_LONG_START)) -# else: -# item_startgap.setBackground(QColor("white")) -# -# item_title: QTableWidgetItem = self.item(row, self.COL_TITLE) -# item_title.setText(track.title) -# -# item_artist: QTableWidgetItem = self.item(row, self.COL_ARTIST) -# item_artist.setText(track.artist) -# -# item_duration: QTableWidgetItem = self.item(row, self.COL_DURATION) -# item_duration.setText(helpers.ms_to_mmss(track.duration)) -# -# self.update_display(session) + + def _update_row(self, session, row: int, track: Tracks) -> None: + """ + Update the passed row with info from the passed track. + """ + + columns['start_time'].idx + item_startgap = self.item(row, columns['start_gap'].idx) + item_startgap.setText(str(track.start_gap)) + if track.start_gap >= 500: + item_startgap.setBackground(QColor(Config.COLOUR_LONG_START)) + else: + item_startgap.setBackground(QColor("white")) + + item_title = self.item(row, columns['title'].idx) + item_title.setText(track.title) + + item_artist = self.item(row, columns['artist'].idx) + item_artist.setText(track.artist) + + item_duration = self.item(row, columns['duration'].idx) + item_duration.setText(helpers.ms_to_mmss(track.duration)) + + self.update_display(session) diff --git a/poetry.lock b/poetry.lock index 817c87d..a2b7d23 100644 --- a/poetry.lock +++ b/poetry.lock @@ -358,6 +358,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pydub-stubs" +version = "0.25.1.0" +description = "Stub-only package containing type information for pydub" +category = "dev" +optional = false +python-versions = ">=3.8,<4.0" + [[package]] name = "pygments" version = "2.11.2" @@ -614,7 +622,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "32b91fc8cb421cc92689db4fc1a4647044714d6e2a72194fe7caf2e25c821b55" +content-hash = "7754808d801630b110a46869b849a6ce205784f587d3c1d4ed2097553e4368c4" [metadata.files] alembic = [ @@ -886,6 +894,7 @@ pydub = [ {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, ] +pydub-stubs = [] pygments = [ {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, diff --git a/pyproject.toml b/pyproject.toml index 734b4f2..a3141bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ PyQt5-stubs = "^5.15.2" mypy = "^0.931" pytest = "^7.0.1" pytest-qt = "^4.0.2" +pydub-stubs = "^0.25.1" [build-system] requires = ["poetry-core>=1.0.0"]