Compare commits
6 Commits
a8fad358b9
...
e711ab84ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e711ab84ab | ||
|
|
67bc3377cb | ||
|
|
8618813197 | ||
|
|
c139215603 | ||
|
|
3831ebb01d | ||
|
|
0cd5d97405 |
@ -1,8 +1,15 @@
|
||||
# Standard library imports
|
||||
import datetime as dt
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
# PyQt imports
|
||||
|
||||
# Third party imports
|
||||
|
||||
# App imports
|
||||
|
||||
|
||||
class Config(object):
|
||||
AUDACITY_TIMEOUT_TENTHS = 100
|
||||
@ -57,7 +64,7 @@ class Config(object):
|
||||
LAST_PLAYED_TODAY_STRING = "Today"
|
||||
LAST_PLAYED_TOOLTIP_DATE_FORMAT = "%a, %d %b %Y"
|
||||
LOG_LEVEL_STDERR = logging.INFO
|
||||
LOG_LEVEL_SYSLOG = logging.INFO
|
||||
LOG_LEVEL_SYSLOG = logging.DEBUG
|
||||
LOG_NAME = "musicmuster"
|
||||
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
|
||||
MAIL_PORT = int(os.environ.get("MAIL_PORT") or 25)
|
||||
|
||||
@ -15,7 +15,7 @@ class DatabaseManager:
|
||||
|
||||
__instance = None
|
||||
|
||||
def __init__(self, database_url, **kwargs):
|
||||
def __init__(self, database_url: str, **kwargs):
|
||||
if DatabaseManager.__instance is None:
|
||||
self.db = Alchemical(database_url, **kwargs)
|
||||
self.db.create_all()
|
||||
@ -24,7 +24,7 @@ class DatabaseManager:
|
||||
raise Exception("Attempted to create a second DatabaseManager instance")
|
||||
|
||||
@staticmethod
|
||||
def get_instance(database_url, **kwargs):
|
||||
def get_instance(database_url: str, **kwargs):
|
||||
if DatabaseManager.__instance is None:
|
||||
DatabaseManager(database_url, **kwargs)
|
||||
return DatabaseManager.__instance
|
||||
|
||||
@ -4,6 +4,7 @@ import os
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import QEvent, Qt
|
||||
from PyQt6.QtGui import QKeyEvent
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog,
|
||||
QListWidgetItem,
|
||||
@ -28,8 +29,7 @@ from helpers import (
|
||||
from log import log
|
||||
from models import db, Settings, Tracks
|
||||
from playlistmodel import PlaylistModel
|
||||
from ui import dlg_TrackSelect_ui
|
||||
from ui import dlg_replace_files_ui
|
||||
from ui import dlg_TrackSelect_ui, dlg_replace_files_ui
|
||||
|
||||
|
||||
class ReplaceFilesDialog(QDialog):
|
||||
@ -386,7 +386,7 @@ class TrackSelectDialog(QDialog):
|
||||
|
||||
event.accept()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||
"""
|
||||
Clear selection on ESC if there is one
|
||||
"""
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# Standard library imports
|
||||
import datetime as dt
|
||||
from email.message import EmailMessage
|
||||
from typing import Any, Dict, Optional
|
||||
@ -9,15 +10,20 @@ import smtplib
|
||||
import ssl
|
||||
import tempfile
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox
|
||||
|
||||
# Third party imports
|
||||
from mutagen.flac import FLAC # type: ignore
|
||||
from mutagen.mp3 import MP3 # type: ignore
|
||||
from pydub import AudioSegment, effects
|
||||
from pydub.utils import mediainfo
|
||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox
|
||||
from tinytag import TinyTag # type: ignore
|
||||
|
||||
# App imports
|
||||
from config import Config
|
||||
from log import log
|
||||
from models import Tracks
|
||||
|
||||
start_time_re = re.compile(r"@\d\d:\d\d")
|
||||
|
||||
@ -266,7 +272,7 @@ def ms_to_mmss(
|
||||
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
|
||||
|
||||
|
||||
def normalise_track(path):
|
||||
def normalise_track(path: str) -> None:
|
||||
"""Normalise track"""
|
||||
|
||||
# Check type
|
||||
@ -319,7 +325,7 @@ def normalise_track(path):
|
||||
os.remove(temp_path)
|
||||
|
||||
|
||||
def send_mail(to_addr, from_addr, subj, body):
|
||||
def send_mail(to_addr: str, from_addr: str, subj: str, body: str) -> None:
|
||||
# From https://docs.python.org/3/library/email.examples.html
|
||||
|
||||
# Create a text/plain message
|
||||
@ -345,7 +351,7 @@ def send_mail(to_addr, from_addr, subj, body):
|
||||
s.quit()
|
||||
|
||||
|
||||
def set_track_metadata(track):
|
||||
def set_track_metadata(track: Tracks) -> None:
|
||||
"""Set/update track metadata in database"""
|
||||
|
||||
audio_metadata = get_audio_metadata(track.path)
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
# Standard library imports
|
||||
import urllib.parse
|
||||
|
||||
import datetime as dt
|
||||
from slugify import slugify # type: ignore
|
||||
from typing import Dict
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import QUrl # type: ignore
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWidgets import QTabWidget
|
||||
from config import Config
|
||||
|
||||
# Third party imports
|
||||
|
||||
# App imports
|
||||
from config import Config
|
||||
from classes import MusicMusterSignals
|
||||
from log import log
|
||||
|
||||
@ -34,7 +39,7 @@ class InfoTabs(QTabWidget):
|
||||
self.last_update[widget] = dt.datetime.now()
|
||||
_ = self.addTab(widget, "")
|
||||
|
||||
def open_in_songfacts(self, title):
|
||||
def open_in_songfacts(self, title: str) -> None:
|
||||
"""Search Songfacts for title"""
|
||||
|
||||
slug = slugify(title, replacements=([["'", ""]]))
|
||||
@ -43,7 +48,7 @@ class InfoTabs(QTabWidget):
|
||||
|
||||
self.open_tab(url, title)
|
||||
|
||||
def open_in_wikipedia(self, title):
|
||||
def open_in_wikipedia(self, title: str) -> None:
|
||||
"""Search Wikipedia for title"""
|
||||
|
||||
str = urllib.parse.quote_plus(title)
|
||||
|
||||
11
app/log.py
11
app/log.py
@ -1,13 +1,18 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import colorlog
|
||||
# Standard library imports
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import stackprinter # type: ignore
|
||||
import sys
|
||||
from traceback import print_exception
|
||||
|
||||
# PyQt imports
|
||||
|
||||
# Third party imports
|
||||
import colorlog
|
||||
import stackprinter # type: ignore
|
||||
|
||||
# App imports
|
||||
from config import Config
|
||||
|
||||
|
||||
|
||||
@ -984,7 +984,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# seconds of playback. Re-enable in update_clocks.
|
||||
|
||||
self.timer10.stop()
|
||||
log.debug("10ms timer disabled", 0)
|
||||
log.debug("10ms timer disabled")
|
||||
|
||||
# If there's currently a track playing, fade it.
|
||||
if track_sequence.current:
|
||||
@ -1044,6 +1044,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
if self.btnPreview.isChecked():
|
||||
# Get track_id for first selected track if there is one
|
||||
track_id = None
|
||||
row_number_and_track_id = self.active_tab().get_selected_row_and_track_id()
|
||||
if row_number_and_track_id:
|
||||
row_number, track_id = row_number_and_track_id
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from log import log
|
||||
|
||||
"""Automate Audacity via mod-script-pipe.
|
||||
|
||||
Pipe Client may be used as a command-line script to send commands to
|
||||
|
||||
@ -1141,14 +1141,14 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Check the track_sequence next, current and previous plrs and
|
||||
# update the row number
|
||||
with db.Session() as session:
|
||||
for ts in [track_sequence.next, track_sequence.current, track_sequence.previous]:
|
||||
for ts in [
|
||||
track_sequence.next,
|
||||
track_sequence.current,
|
||||
track_sequence.previous,
|
||||
]:
|
||||
if ts and ts.row_number:
|
||||
plr = session.get(PlaylistRows, ts.plr_id)
|
||||
if plr and plr.plr_rownum != ts.row_number:
|
||||
log.error(
|
||||
"reset_track_sequence_row_numbers: "
|
||||
f"from {ts=} to {plr.plr_rownum=}"
|
||||
)
|
||||
ts.row_number = plr.plr_rownum
|
||||
|
||||
self.update_track_times()
|
||||
|
||||
@ -271,8 +271,21 @@ class PlaylistTab(QTableView):
|
||||
|
||||
from_rows = self.selected_model_row_numbers()
|
||||
to_index = self.indexAt(event.position().toPoint())
|
||||
to_model_row = self.proxy_model.mapToSource(to_index).row()
|
||||
log.info(f"PlaylistTab.dropEvent(): {from_rows=}, {to_index=}, {to_model_row=}")
|
||||
if (
|
||||
self.dropIndicatorPosition()
|
||||
== QAbstractItemView.DropIndicatorPosition.BelowItem
|
||||
):
|
||||
proxy_index = self.proxy_model.createIndex(
|
||||
to_index.row() + 1,
|
||||
to_index.column(),
|
||||
to_index.internalId(),
|
||||
)
|
||||
else:
|
||||
proxy_index = to_index
|
||||
to_model_row = self.proxy_model.mapToSource(proxy_index).row()
|
||||
log.debug(
|
||||
f"PlaylistTab.dropEvent(): {from_rows=}, {proxy_index=}, {to_model_row=}"
|
||||
)
|
||||
|
||||
if (
|
||||
0 <= min(from_rows) <= self.source_model.rowCount()
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Script to manage renaming existing files in given directory and
|
||||
# propagating that change to database. Typical usage: renaming files
|
||||
# from 'title.mp3' to title - artist.mp3'
|
||||
#
|
||||
# Actions:
|
||||
#
|
||||
# - record all filenames and inode numbers
|
||||
# - external: rename the files
|
||||
# - update records with new filenames for each inode number
|
||||
# - update external database with new paths
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
PHASE = 2
|
||||
|
||||
# Check file of same name exists in parent directory
|
||||
source_dir = '/home/kae/tmp/Singles' # os.getcwd()
|
||||
db = "/home/kae/tmp/singles.sqlite"
|
||||
|
||||
|
||||
def main():
|
||||
with sqlite3.connect(db) as connection:
|
||||
cursor = connection.cursor()
|
||||
if PHASE == 1:
|
||||
cursor.execute(
|
||||
"CREATE TABLE IF NOT EXISTS mp3s "
|
||||
"(inode INTEGER, oldname TEXT, newname TEXT)"
|
||||
)
|
||||
|
||||
for fname in os.listdir(source_dir):
|
||||
fullpath = os.path.join(source_dir, fname)
|
||||
inode = os.stat(fullpath).st_ino
|
||||
sql = f'INSERT INTO mp3s VALUES ({inode}, "{fname}", "")'
|
||||
cursor.execute(sql)
|
||||
|
||||
if PHASE == 2:
|
||||
for fname in os.listdir(source_dir):
|
||||
fullpath = os.path.join(source_dir, fname)
|
||||
inode = os.stat(fullpath).st_ino
|
||||
sql = (
|
||||
f'UPDATE mp3s SET newname = "{fname}" WHERE inode={inode}'
|
||||
)
|
||||
try:
|
||||
cursor.execute(sql)
|
||||
except sqlite3.OperationalError:
|
||||
print(f"Error with {inode} -> {fname}")
|
||||
|
||||
cursor.close()
|
||||
|
||||
|
||||
main()
|
||||
@ -47,14 +47,14 @@ class _AddFadeCurve(QObject):
|
||||
track_path: str,
|
||||
track_fade_at: int,
|
||||
track_silence_at: int,
|
||||
):
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.track_manager = track_manager
|
||||
self.track_path = track_path
|
||||
self.track_fade_at = track_fade_at
|
||||
self.track_silence_at = track_silence_at
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Create fade curve and add to PlaylistTrack object
|
||||
"""
|
||||
@ -102,7 +102,7 @@ class _FadeCurve:
|
||||
if self.GraphWidget:
|
||||
self.GraphWidget.clear()
|
||||
|
||||
def plot(self):
|
||||
def plot(self) -> None:
|
||||
self.curve = self.GraphWidget.plot(self.graph_array)
|
||||
self.curve.setPen(Config.FADE_CURVE_FOREGROUND)
|
||||
|
||||
@ -328,7 +328,7 @@ class _Music:
|
||||
if self.player:
|
||||
self.player.set_position(position)
|
||||
|
||||
def set_volume(self, volume=None, set_default=True) -> None:
|
||||
def set_volume(self, volume: Optional[int] = None, set_default: bool = True) -> None:
|
||||
"""Set maximum volume used for player"""
|
||||
|
||||
if not self.player:
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Standard library imports
|
||||
import os
|
||||
|
||||
# PyQt imports
|
||||
|
||||
# Third party imports
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
# App imports
|
||||
from config import Config
|
||||
from helpers import (
|
||||
get_tags,
|
||||
@ -10,7 +16,7 @@ from log import log
|
||||
from models import Tracks
|
||||
|
||||
|
||||
def check_db(session):
|
||||
def check_db(session: Session):
|
||||
"""
|
||||
Database consistency check.
|
||||
|
||||
@ -78,7 +84,7 @@ def check_db(session):
|
||||
print("There were more paths than listed that were not found")
|
||||
|
||||
|
||||
def update_bitrates(session):
|
||||
def update_bitrates(session: Session):
|
||||
"""
|
||||
Update bitrates on all tracks in database
|
||||
"""
|
||||
|
||||
101
archive/dragdrop.py
Executable file
101
archive/dragdrop.py
Executable file
@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget
|
||||
|
||||
import sys
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QTableWidget,
|
||||
QAbstractItemView,
|
||||
QTableWidgetItem,
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QApplication,
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
|
||||
class TableWidgetDragRows(QTableWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setDragEnabled(True)
|
||||
self.setAcceptDrops(True)
|
||||
self.viewport().setAcceptDrops(True)
|
||||
self.setDragDropOverwriteMode(False)
|
||||
self.setDropIndicatorShown(True)
|
||||
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
||||
self.DropIndicatorPosition(QAbstractItemView.DropIndicatorPosition.BelowItem)
|
||||
|
||||
def dropEvent(self, event):
|
||||
if event.source() == self:
|
||||
rows = set([mi.row() for mi in self.selectedIndexes()])
|
||||
targetRow = self.indexAt(event.position().toPoint()).row()
|
||||
if self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.BelowItem:
|
||||
targetRow += 1
|
||||
rows.discard(targetRow)
|
||||
rows = sorted(rows)
|
||||
if not rows:
|
||||
return
|
||||
if targetRow == -1:
|
||||
targetRow = self.rowCount()
|
||||
for _ in range(len(rows)):
|
||||
self.insertRow(targetRow)
|
||||
rowMapping = dict() # Src row to target row.
|
||||
for idx, row in enumerate(rows):
|
||||
if row < targetRow:
|
||||
rowMapping[row] = targetRow + idx
|
||||
else:
|
||||
rowMapping[row + len(rows)] = targetRow + idx
|
||||
colCount = self.columnCount()
|
||||
for srcRow, tgtRow in sorted(rowMapping.items()):
|
||||
for col in range(0, colCount):
|
||||
self.setItem(tgtRow, col, self.takeItem(srcRow, col))
|
||||
for row in reversed(sorted(rowMapping.keys())):
|
||||
self.removeRow(row)
|
||||
event.accept()
|
||||
return
|
||||
|
||||
|
||||
class Window(QWidget):
|
||||
def __init__(self):
|
||||
super(Window, self).__init__()
|
||||
|
||||
layout = QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.table_widget = TableWidgetDragRows()
|
||||
layout.addWidget(self.table_widget)
|
||||
|
||||
# setup table widget
|
||||
self.table_widget.setColumnCount(2)
|
||||
self.table_widget.setHorizontalHeaderLabels(["Type", "Name"])
|
||||
|
||||
items = [
|
||||
("Red", "Toyota"),
|
||||
("Blue", "RV"),
|
||||
("Green", "Beetle"),
|
||||
("Silver", "Chevy"),
|
||||
("Black", "BMW"),
|
||||
]
|
||||
self.table_widget.setRowCount(len(items))
|
||||
for i, (color, model) in enumerate(items):
|
||||
item_flags = Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsDragEnabled
|
||||
colour_item = QTableWidgetItem(color)
|
||||
colour_item.setFlags(item_flags)
|
||||
model_item = QTableWidgetItem(model)
|
||||
model_item.setFlags(item_flags)
|
||||
self.table_widget.setItem(i, 0, QTableWidgetItem(color))
|
||||
self.table_widget.setItem(i, 1, QTableWidgetItem(model))
|
||||
|
||||
self.resize(400, 400)
|
||||
self.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
window = Window()
|
||||
sys.exit(app.exec())
|
||||
7
file_header.txt
Normal file
7
file_header.txt
Normal file
@ -0,0 +1,7 @@
|
||||
# Standard library imports
|
||||
|
||||
# PyQt imports
|
||||
|
||||
# Third party imports
|
||||
|
||||
# App imports
|
||||
Loading…
Reference in New Issue
Block a user