From d50120b3e53162d7b3fb2cd81670914340e53117 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sat, 20 Mar 2021 12:47:31 +0000 Subject: [PATCH] experimentation --- app.py | 187 ++++++++------ app.py.reference | 45 ++++ audactious-control.sh | 15 ++ dlg_search_database_ui.py | 48 ++++ main_window_ui.py | 218 +++++++++++++++++ notes.otl | 17 ++ songdb.py | 2 +- sys | 0 ui/dlg_SearchDatabase.ui | 91 +++++++ ui/main_window.ui | 498 +++++++++++++++++++------------------- ui/play_icon-64.jpeg | Bin 0 -> 8875 bytes ui/play_icon.jpeg | Bin 0 -> 4704 bytes 12 files changed, 798 insertions(+), 323 deletions(-) create mode 100644 app.py.reference create mode 100755 audactious-control.sh create mode 100644 dlg_search_database_ui.py create mode 100644 main_window_ui.py create mode 100644 notes.otl create mode 100644 sys create mode 100644 ui/dlg_SearchDatabase.ui create mode 100644 ui/play_icon-64.jpeg create mode 100644 ui/play_icon.jpeg diff --git a/app.py b/app.py index 6bee8ec..e6c3e99 100755 --- a/app.py +++ b/app.py @@ -12,6 +12,10 @@ from PyQt5.uic import loadUi from threading import Timer from time import sleep from timeloop import Timeloop +from tinytag import TinyTag + +from main_window_ui import Ui_MainWindow +from dlg_search_database_ui import Ui_Dialog class RepeatedTimer: @@ -20,7 +24,7 @@ class RepeatedTimer: self.interval = interval self.function = function self.args = args - self.kwargs = kwargs + self.kwargs = kwargsdlg_search_database_ui self.is_running = False self.start() @@ -39,23 +43,28 @@ class RepeatedTimer: self._timer.cancel() self.is_running = False - class Track: def __init__(self, path): self.path = path audio = self.get_audio_segment(path) - self.length = int(len(audio)) 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" silence_at={sedlg_search_database_uilf.silence_at}" + f" title={self.title}" + f" artist={self.artist}" ) def get_audio_segment(self, path): @@ -74,7 +83,7 @@ class Track: 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 - +dlg_search_database_ui https://github.com/jiaaro/pydub/blob/master/pydub/silence.py """ @@ -92,7 +101,7 @@ class Track: """ 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 + 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 """ @@ -115,20 +124,106 @@ class Track: return self.fade_point(audio_segment, silence_threshold, chunk_size) +ROOT = "/home/kae/music/" + + + +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 ms_to_mmss(self, 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}" + + def connectSignalsSlots(self): + self.fileButton.clicked.connect(self.selectFile) + self.databaseButton.clicked.connect(self.selectDatabase) + # import ipdb; ipdb.set_trace() + # self.playlist.columnResized.connect(self.playlistresize) + # self.playlist.horizontalHeader().sectionResized.connect(self.kae) + # import ipdb; ipdb.set_trace() + # self.playlist.viewport().installEventFilter(self) + # self.playlist.horizontalHeader().sectionClicked.connect(self.kae2) + # x = self.playlist.horizontalHeader() + # import ipdb; ipdb.set_trace() + + def selectDatabase(self): + dlg = DbDialog() + dlg.exec() + + def selectFile(self): + dlg = QFileDialog() + dlg.setFileMode(QFileDialog.ExistingFile) + dlg.setViewMode(QFileDialog.Detail) + Gdlg.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): + pl = self.playlist + row = pl.rowCount() + pl.insertRow(row) + item = QTableWidgetItem(track.title) + pl.setItem(row, 1, item) + item = QTableWidgetItem(track.artist) + pl.setItem(row, 2, item) + item = QTableWidgetItem(self.ms_to_mmss(track.length)) + pl.setItem(row, 3, item) + item = QTableWidgetItem(track.path) + pl.setItem(row, 6, item) + +class DbDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_Dialog() + self.ui.setupUi(self) + import ipdb; ipdb.set_trace() + # self.lineEdit.clicked.connect(self.selectFile) + + + + +if __name__ == "__main__": + app = QApplication(sys.argv) + win = Window() + win.show() + sys.exit(app.exec()) + + # tl = Timeloop() # -#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}" # # ## @tl.job(interval=timedelta(seconds=1)) @@ -174,45 +269,6 @@ class Track: # rt.stop() # better in a try/finally block to make sure the program ends! # print("End") - - -ROOT = "/home/kae/music/" - -from main_window_ui import Ui_MainWindow - - -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, 381) - self.playlist.setColumnWidth(2, 322) - self.playlist.setColumnWidth(3, 78) - self.playlist.setColumnWidth(4, 68) - self.playlist.setColumnWidth(5, 47) - self.playlist.setColumnWidth(6, 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.pushButton.clicked.connect(self.btnpush) - # import ipdb; ipdb.set_trace() - # self.playlist.columnResized.connect(self.playlistresize) - # self.playlist.horizontalHeader().sectionResized.connect(self.kae) - # import ipdb; ipdb.set_trace() - # self.playlist.viewport().installEventFilter(self) - # self.playlist.horizontalHeader().sectionClicked.connect(self.kae2) - # x = self.playlist.horizontalHeader() - # import ipdb; ipdb.set_trace() - #def kae2(self, index): # print(f"table header click, index={index}") @@ -244,20 +300,3 @@ class Window(QMainWindow, Ui_MainWindow): # # regardless, just do the default # return super().eventFilter(obj, event) - def btnpush(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) - - -if __name__ == "__main__": - app = QApplication(sys.argv) - win = Window() - win.show() - sys.exit(app.exec()) diff --git a/app.py.reference b/app.py.reference new file mode 100644 index 0000000..79edb06 --- /dev/null +++ b/app.py.reference @@ -0,0 +1,45 @@ +import sys + +from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow, QMessageBox +from PyQt5.uic import loadUi + +from main_window_ui import Ui_MainWindow + + +class Window(QMainWindow, Ui_MainWindow): + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) + self.connectSignalsSlots() + + def connectSignalsSlots(self): + self.action_Exit.triggered.connect(self.close) + self.action_Find_Replace.triggered.connect(self.findAndReplace) + self.action_About.triggered.connect(self.about) + + def findAndReplace(self): + dialog = FindReplaceDialog(self) + dialog.exec() + + def about(self): + QMessageBox.about( + self, + "About Sample Editor", + "

A sample text editor app built with:

" + "

- PyQt

" + "

- Qt Designer

" + "

- Python

", + ) + + +class FindReplaceDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + loadUi("ui/find_replace.ui", self) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + win = Window() + win.show() + sys.exit(app.exec()) diff --git a/audactious-control.sh b/audactious-control.sh new file mode 100755 index 0000000..b7e868e --- /dev/null +++ b/audactious-control.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +a="/usr/bin/audtool" + +# Advance playlist when track stops +while :; do + $a playback-stopped + if [ $? -eq 0 ]; then + break + fi + sleep 1 +done +$a playlist-advance +exit 0 + diff --git a/dlg_search_database_ui.py b/dlg_search_database_ui.py new file mode 100644 index 0000000..29bd471 --- /dev/null +++ b/dlg_search_database_ui.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui/dlg_SearchDatabase.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(383, 272) + self.widget = QtWidgets.QWidget(Dialog) + self.widget.setGeometry(QtCore.QRect(10, 20, 361, 242)) + self.widget.setObjectName("widget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.widget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.label = QtWidgets.QLabel(self.widget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 1) + self.lineEdit = QtWidgets.QLineEdit(self.widget) + self.lineEdit.setObjectName("lineEdit") + self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1) + self.verticalLayout.addLayout(self.gridLayout) + self.listWidget = QtWidgets.QListWidget(self.widget) + self.listWidget.setObjectName("listWidget") + self.verticalLayout.addWidget(self.listWidget) + self.buttonBox = QtWidgets.QDialogButtonBox(self.widget) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(Dialog) + self.buttonBox.accepted.connect(Dialog.accept) + self.buttonBox.rejected.connect(Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.label.setText(_translate("Dialog", "Title:")) + diff --git a/main_window_ui.py b/main_window_ui.py new file mode 100644 index 0000000..68b5ed1 --- /dev/null +++ b/main_window_ui.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui/main_window.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(768, 600) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) + self.verticalLayout.setObjectName("verticalLayout") + self.gridLayout_5 = QtWidgets.QGridLayout() + self.gridLayout_5.setHorizontalSpacing(10) + self.gridLayout_5.setObjectName("gridLayout_5") + self.start_box = QtWidgets.QGroupBox(self.centralwidget) + self.start_box.setObjectName("start_box") + self.gridLayout = QtWidgets.QGridLayout(self.start_box) + self.gridLayout.setObjectName("gridLayout") + self.label_elapsed_timer = QtWidgets.QLabel(self.start_box) + font = QtGui.QFont() + font.setFamily("Sans") + font.setPointSize(40) + font.setBold(True) + font.setWeight(75) + self.label_elapsed_timer.setFont(font) + self.label_elapsed_timer.setObjectName("label_elapsed_timer") + self.gridLayout.addWidget(self.label_elapsed_timer, 0, 0, 1, 1) + self.label_start_tod = QtWidgets.QLabel(self.start_box) + font = QtGui.QFont() + font.setFamily("DejaVu Sans") + font.setPointSize(16) + self.label_start_tod.setFont(font) + self.label_start_tod.setScaledContents(False) + self.label_start_tod.setObjectName("label_start_tod") + self.gridLayout.addWidget(self.label_start_tod, 1, 0, 1, 1) + self.gridLayout_5.addWidget(self.start_box, 0, 0, 1, 1) + self.fade_box = QtWidgets.QGroupBox(self.centralwidget) + self.fade_box.setObjectName("fade_box") + self.gridLayout_2 = QtWidgets.QGridLayout(self.fade_box) + self.gridLayout_2.setObjectName("gridLayout_2") + self.label_fade_timer = QtWidgets.QLabel(self.fade_box) + font = QtGui.QFont() + font.setFamily("Sans") + font.setPointSize(40) + font.setBold(True) + font.setWeight(75) + self.label_fade_timer.setFont(font) + self.label_fade_timer.setObjectName("label_fade_timer") + self.gridLayout_2.addWidget(self.label_fade_timer, 0, 0, 1, 1) + self.label_fade_tod = QtWidgets.QLabel(self.fade_box) + font = QtGui.QFont() + font.setFamily("DejaVu Sans") + font.setPointSize(16) + self.label_fade_tod.setFont(font) + self.label_fade_tod.setScaledContents(False) + self.label_fade_tod.setObjectName("label_fade_tod") + self.gridLayout_2.addWidget(self.label_fade_tod, 1, 0, 1, 1) + self.gridLayout_5.addWidget(self.fade_box, 0, 1, 1, 1) + self.silent_box = QtWidgets.QGroupBox(self.centralwidget) + self.silent_box.setStyleSheet("background-color: rgb(252, 233, 79);") + self.silent_box.setObjectName("silent_box") + self.gridLayout_3 = QtWidgets.QGridLayout(self.silent_box) + self.gridLayout_3.setObjectName("gridLayout_3") + self.label_silent_timer = QtWidgets.QLabel(self.silent_box) + font = QtGui.QFont() + font.setFamily("Sans") + font.setPointSize(40) + font.setBold(True) + font.setWeight(75) + self.label_silent_timer.setFont(font) + self.label_silent_timer.setObjectName("label_silent_timer") + self.gridLayout_3.addWidget(self.label_silent_timer, 0, 0, 1, 1) + self.label_silent_tod = QtWidgets.QLabel(self.silent_box) + font = QtGui.QFont() + font.setFamily("DejaVu Sans") + font.setPointSize(16) + self.label_silent_tod.setFont(font) + self.label_silent_tod.setScaledContents(False) + self.label_silent_tod.setObjectName("label_silent_tod") + self.gridLayout_3.addWidget(self.label_silent_tod, 1, 0, 1, 1) + self.gridLayout_5.addWidget(self.silent_box, 0, 2, 1, 1) + self.end_box = QtWidgets.QGroupBox(self.centralwidget) + self.end_box.setObjectName("end_box") + self.gridLayout_4 = QtWidgets.QGridLayout(self.end_box) + self.gridLayout_4.setObjectName("gridLayout_4") + self.label_end_timer = QtWidgets.QLabel(self.end_box) + font = QtGui.QFont() + font.setFamily("Sans") + font.setPointSize(40) + font.setBold(True) + font.setWeight(75) + self.label_end_timer.setFont(font) + self.label_end_timer.setObjectName("label_end_timer") + self.gridLayout_4.addWidget(self.label_end_timer, 0, 0, 1, 1) + self.label_end_tod = QtWidgets.QLabel(self.end_box) + font = QtGui.QFont() + font.setFamily("DejaVu Sans") + font.setPointSize(16) + self.label_end_tod.setFont(font) + self.label_end_tod.setScaledContents(False) + self.label_end_tod.setObjectName("label_end_tod") + self.gridLayout_4.addWidget(self.label_end_tod, 1, 0, 1, 1) + self.gridLayout_5.addWidget(self.end_box, 0, 3, 1, 1) + self.verticalLayout.addLayout(self.gridLayout_5) + self.current_track = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setFamily("Sans") + font.setPointSize(20) + self.current_track.setFont(font) + self.current_track.setStyleSheet("background-color: rgb(138, 226, 52);\n" +"border: 1px solid rgb(85, 87, 83);") + self.current_track.setObjectName("current_track") + self.verticalLayout.addWidget(self.current_track) + self.playlist = QtWidgets.QTableWidget(self.centralwidget) + self.playlist.setAlternatingRowColors(True) + self.playlist.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection) + self.playlist.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.playlist.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.playlist.setRowCount(0) + self.playlist.setColumnCount(8) + self.playlist.setObjectName("playlist") + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(0, item) + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(1, item) + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(2, item) + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(3, item) + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(4, item) + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(5, item) + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(6, item) + item = QtWidgets.QTableWidgetItem() + self.playlist.setHorizontalHeaderItem(7, item) + self.verticalLayout.addWidget(self.playlist) + self.fileButton = QtWidgets.QPushButton(self.centralwidget) + self.fileButton.setObjectName("fileButton") + self.verticalLayout.addWidget(self.fileButton) + self.databaseButton = QtWidgets.QPushButton(self.centralwidget) + self.databaseButton.setObjectName("databaseButton") + self.verticalLayout.addWidget(self.databaseButton) + self.current_time = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setFamily("DejaVu Sans") + font.setPointSize(25) + font.setBold(True) + font.setWeight(75) + self.current_time.setFont(font) + self.current_time.setObjectName("current_time") + self.verticalLayout.addWidget(self.current_time) + self.playlist.raise_() + self.current_time.raise_() + self.fileButton.raise_() + self.current_track.raise_() + self.databaseButton.raise_() + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 768, 18)) + self.menubar.setObjectName("menubar") + self.menuFile = QtWidgets.QMenu(self.menubar) + self.menuFile.setObjectName("menuFile") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setStyleSheet("background-color: rgb(211, 215, 207);") + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + self.menubar.addAction(self.menuFile.menuAction()) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.start_box.setTitle(_translate("MainWindow", "Elapsed")) + self.label_elapsed_timer.setText(_translate("MainWindow", "2:46")) + self.label_start_tod.setText(_translate("MainWindow", "10:17:37")) + self.fade_box.setTitle(_translate("MainWindow", "Fade at")) + self.label_fade_timer.setText(_translate("MainWindow", "0:53")) + self.label_fade_tod.setText(_translate("MainWindow", "10:21:23")) + self.silent_box.setTitle(_translate("MainWindow", "Silent at")) + self.label_silent_timer.setText(_translate("MainWindow", "0:58")) + self.label_silent_tod.setText(_translate("MainWindow", "10:21:28")) + self.end_box.setTitle(_translate("MainWindow", "End at")) + self.label_end_timer.setText(_translate("MainWindow", "1:00")) + self.label_end_tod.setText(_translate("MainWindow", "10:21:30")) + self.current_track.setText(_translate("MainWindow", "After the goldrush - Neil Young [3:46]")) + item = self.playlist.horizontalHeaderItem(0) + item.setText(_translate("MainWindow", "Index")) + item = self.playlist.horizontalHeaderItem(1) + item.setText(_translate("MainWindow", "Skip silence")) + item = self.playlist.horizontalHeaderItem(2) + item.setText(_translate("MainWindow", "Title")) + item = self.playlist.horizontalHeaderItem(3) + item.setText(_translate("MainWindow", "Artist")) + item = self.playlist.horizontalHeaderItem(4) + item.setText(_translate("MainWindow", "Duration")) + item = self.playlist.horizontalHeaderItem(5) + item.setText(_translate("MainWindow", "End time")) + item = self.playlist.horizontalHeaderItem(6) + item.setText(_translate("MainWindow", "Autoplay next")) + item = self.playlist.horizontalHeaderItem(7) + item.setText(_translate("MainWindow", "Path")) + self.fileButton.setText(_translate("MainWindow", "Select file")) + self.databaseButton.setText(_translate("MainWindow", "Database")) + self.current_time.setText(_translate("MainWindow", "10:20:30")) + self.menuFile.setTitle(_translate("MainWindow", "Fi&le")) + diff --git a/notes.otl b/notes.otl new file mode 100644 index 0000000..2e18900 --- /dev/null +++ b/notes.otl @@ -0,0 +1,17 @@ +Playlist columns + Silence skip + Title + Artist + Duration to silence + End time + Autoplay next + Path +Names (app and db) + Beacon + Music Muster + Music Tap + Oryx + Accord + PlayStack +Status bar + Total remaining playlist time diff --git a/songdb.py b/songdb.py index e28f52e..6cd5198 100755 --- a/songdb.py +++ b/songdb.py @@ -105,7 +105,7 @@ def update_db(session): tag = TinyTag.get(path) track.title = tag.title track.artist = tag.artist - track.length = tag.duration * 1000 + track.length = int(tag.duration * 1000) track.mtime = os.path.getmtime(path) elif ext not in [".jpg"]: print(f"Unrecognised file type: {path}") diff --git a/sys b/sys new file mode 100644 index 0000000..e69de29 diff --git a/ui/dlg_SearchDatabase.ui b/ui/dlg_SearchDatabase.ui new file mode 100644 index 0000000..d303eec --- /dev/null +++ b/ui/dlg_SearchDatabase.ui @@ -0,0 +1,91 @@ + + + Dialog + + + + 0 + 0 + 383 + 272 + + + + Dialog + + + + + 10 + 20 + 361 + 242 + + + + + + + + + Title: + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ui/main_window.ui b/ui/main_window.ui index 877c1e2..7c77d49 100644 --- a/ui/main_window.ui +++ b/ui/main_window.ui @@ -6,7 +6,7 @@ 0 0 - 800 + 768 600 @@ -16,260 +16,265 @@ - + + + 10 + - - - - Sans - 20 - - - - background-color: rgb(138, 226, 52); -border: 1px solid rgb(85, 87, 83); - - - After the goldrush - Neil Young [3:46] + + + Elapsed + + + + + + Sans + 40 + 75 + true + + + + 2:46 + + + + + + + + DejaVu Sans + 16 + + + + 10:17:37 + + + false + + + + - - - - 10 + + + + Fade at - - - - Elapsed - - - - - - - Sans - 40 - 75 - true - - - - 2:46 - - - - - - - - DejaVu Sans - 16 - - - - 10:17:37 - - - false - - - - - - - - - - Fade at - - - - - - - Sans - 40 - 75 - true - - - - 0:53 - - - - - - - - DejaVu Sans - 16 - - - - 10:21:23 - - - false - - - - - - - - - - background-color: rgb(252, 233, 79); - - - Silent at - - - - - - - Sans - 40 - 75 - true - - - - 0:58 - - - - - - - - DejaVu Sans - 16 - - - - 10:21:28 - - - false - - - - - - - - - - End at - - - - - - - Sans - 40 - 75 - true - - - - 1:00 - - - - - - - - DejaVu Sans - 16 - - - - 10:21:30 - - - false - - - - - - - + + + + + + Sans + 40 + 75 + true + + + + 0:53 + + + + + + + + DejaVu Sans + 16 + + + + 10:21:23 + + + false + + + + + - - - - true + + + + background-color: rgb(252, 233, 79); - - QAbstractItemView::ContiguousSelection + + Silent at - - QAbstractItemView::SelectRows + + + + + + Sans + 40 + 75 + true + + + + 0:58 + + + + + + + + DejaVu Sans + 16 + + + + 10:21:28 + + + false + + + + + + + + + + End at - - QAbstractItemView::ScrollPerPixel - - - 3 - - - 7 - - - - - - - Skip silence - - - - - Title - - - - - Artist - - - - - Duration - - - - - End time - - - - - Autoplay next - - - - - Path - - + + + + + + Sans + 40 + 75 + true + + + + 1:00 + + + + + + + + DejaVu Sans + 16 + + + + 10:21:30 + + + false + + + + - + + + + Sans + 20 + + + + background-color: rgb(138, 226, 52); +border: 1px solid rgb(85, 87, 83); + - PushButton + After the goldrush - Neil Young [3:46] + + + + + + + true + + + QAbstractItemView::ContiguousSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + 0 + + + 8 + + + + Index + + + + + Skip silence + + + + + Title + + + + + Artist + + + + + Duration + + + + + End time + + + + + Autoplay next + + + + + Path + + + + + + + + Select file + + + + + + + Database @@ -289,22 +294,19 @@ border: 1px solid rgb(85, 87, 83); - current_track - start_box - fade_box - silent_box - end_box + layoutWidget playlist current_time - pushButton - + fileButton + current_track + databaseButton 0 0 - 800 + 768 18 diff --git a/ui/play_icon-64.jpeg b/ui/play_icon-64.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b2c6e92c0c02d8f7ffa31d17372a8d4b061e16a4 GIT binary patch literal 8875 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 literal 0 HcmV?d00001 diff --git a/ui/play_icon.jpeg b/ui/play_icon.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8bdac3f662324cf38e08805a6d2354c9a45e432b GIT binary patch literal 4704 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 tD(=Ot?8R(DsRPyvelmx;$vA