#!/usr/bin/python3 import sys from pydub import AudioSegment from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow from PyQt5.QtWidgets import QTableWidgetItem, QFileDialog, QListWidgetItem from threading import Timer from tinytag import TinyTag from main_window_ui import Ui_MainWindow from dlg_search_database_ui import Ui_Dialog from model import Tracks class RepeatedTimer: def __init__(self, interval, function, *args, **kwargs): self._timer = None self.interval = interval self.function = function self.args = args self.kwargs = kwargs self.is_running = False self.start() def _run(self): self.is_running = False self.start() self.function(*self.args, **self.kwargs) def start(self): if not self.is_running: self._timer = Timer(self.interval, self._run) self._timer.start() self.is_running = True def stop(self): self._timer.cancel() self.is_running = False class Track: def __init__(self, path): self.path = path audio = self.get_audio_segment(path) self.start_gap = self.leading_silence(audio) self.fade_at = self.fade_point(audio) self.silence_at = self.trailing_silence(audio) tag = TinyTag.get(path) self.title = tag.title self.artist = tag.artist self.length = tag.duration * 1000 print( f" path={self.path}" f" length={self.length}" f" start_gap={self.start_gap}" f" fade_at={self.fade_at}" f" silence_at={self.silence_at}" f" title={self.title}" f" artist={self.artist}" ) def get_audio_segment(self, path): 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 leading_silence(self, audio_segment, silence_threshold=-50.0, chunk_size=10): """ 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 = 0 # ms assert chunk_size > 0 # to avoid infinite loop while ( audio_segment[trim_ms:trim_ms + chunk_size].dBFS < 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 fade_point(self, audio_segment, fade_threshold=-20.0, chunk_size=10): """ Returns the millisecond/index of the point where the fade is down to fade_threshold and doesn't get louder again. audio_segment - the segment 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 = audio_segment.duration_seconds * 1000 # ms trim_ms = segment_length - chunk_size while ( audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold and trim_ms > 0): 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 trailing_silence(self, audio_segment, silence_threshold=-50.0, chunk_size=10): return self.fade_point(audio_segment, silence_threshold, chunk_size) ROOT = "/home/kae/music/" def ms_to_mmss(ms, decimals=0): if not ms: return "-" if ms < 0: sign = "-" else: sign = "" minutes, remainder = divmod(ms, 60 * 1000) seconds = remainder / 1000 return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}" class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.connectSignalsSlots() self.resize(1599, 981) self.playlist.setColumnWidth(0, 0) self.playlist.setColumnWidth(1, 70) self.playlist.setColumnWidth(2, 381) self.playlist.setColumnWidth(3, 322) self.playlist.setColumnWidth(4, 78) self.playlist.setColumnWidth(5, 68) self.playlist.setColumnWidth(6, 47) self.playlist.setColumnWidth(7, 577) def __del__(self): for column in range(self.playlist.columnCount()): print(f"Column {column}: {self.playlist.columnWidth(column)}") print(f"Window height: {self.height()} Window width: {self.width()}") def connectSignalsSlots(self): self.fileButton.clicked.connect(self.selectFile) self.databaseButton.clicked.connect(self.selectDatabase) self.actionPlay_selected.triggered.connect(self.play_selected) def selectDatabase(self): dlg = DbDialog(self) dlg.exec() def play_selected(self): if self.playlist.selectionModel().hasSelection(): row = self.playlist.currentRow() print(f"Play id={self.playlist.item(row, 0).text()}") print("play selected") def selectFile(self): dlg = QFileDialog() dlg.setFileMode(QFileDialog.ExistingFile) dlg.setViewMode(QFileDialog.Detail) dlg.setDirectory(ROOT) dlg.setNameFilter("Music files (*.flac *.mp3)") if dlg.exec_(): for fname in dlg.selectedFiles(): track = Track(fname) self.add_to_playlist(track) def add_to_playlist(self, track): """ Add track to playlist track is an instance of Track """ pl = self.playlist row = pl.rowCount() pl.insertRow(row) item = QTableWidgetItem(str(track.id)) pl.setItem(row, 0, item) item = QTableWidgetItem(str(track.start_gap)) pl.setItem(row, 1, item) item = QTableWidgetItem(track.title) pl.setItem(row, 2, item) item = QTableWidgetItem(track.artist) pl.setItem(row, 3, item) item = QTableWidgetItem(ms_to_mmss(track.duration)) pl.setItem(row, 4, item) item = QTableWidgetItem(track.path) pl.setItem(row, 7, item) class DbDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.ui = Ui_Dialog() self.ui.setupUi(self) self.ui.lineEdit.textEdited.connect(self.chars_typed) self.ui.listWidget.itemDoubleClicked.connect(self.listdclick) def chars_typed(self, s): if len(s) >= 3: matches = Tracks.search_titles(s) if matches: self.ui.listWidget.clear() for track in matches: t = QListWidgetItem() t.setText( f"{track.title} - {track.artist} " f"[{ms_to_mmss(track.duration)}]" ) t.setData(Qt.UserRole, track.id) self.ui.listWidget.addItem(t) def listdclick(self, entry): track_id = entry.data(Qt.UserRole) track = Tracks.track_from_id(track_id) self.parent().add_to_playlist(track) if __name__ == "__main__": app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec())