Build in replace_file functionality

Major rewrite of file importing

Fixes #141
This commit is contained in:
Keith Edmunds 2024-05-03 22:40:21 +01:00
parent 6aa09bf28a
commit a24ff76b6b
13 changed files with 677 additions and 289 deletions

View File

@ -1,6 +1,6 @@
# Standard library imports
from dataclasses import dataclass
from typing import Optional
from dataclasses import dataclass, field
from typing import Any, Optional
import datetime as dt
# PyQt imports
@ -203,6 +203,19 @@ class PlaylistTrack:
self.fade_graph_start_updates = now + dt.timedelta(milliseconds=update_graph_at_ms)
@dataclass
class TrackFileData:
"""
Simple class to track details changes to a track file
"""
source_file_path: str
track_id: int = 0
track_path: Optional[str] = None
tags: dict[str, Any] = field(default_factory=dict)
audio_metadata: dict[str, str | int | float] = field(default_factory=dict)
class AddFadeCurve(QObject):
"""
Initialising a fade curve introduces a noticeable delay so carry out in

View File

@ -78,6 +78,7 @@ class Config(object):
OBS_PASSWORD = "auster"
OBS_PORT = 4455
PLAY_SETTLE = 500000
REPLACE_FILES_DEFAULT_SOURCE = "/home/kae/music/Singles/tmp"
RETURN_KEY_DEBOUNCE_MS = 500
ROOT = os.environ.get("ROOT") or "/home/kae/music"
ROWS_FROM_ZERO = True
@ -94,3 +95,4 @@ class Config(object):
# These rely on earlier definitions
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
REPLACE_FILES_DEFAULT_DESTINATION = os.path.dirname(REPLACE_FILES_DEFAULT_SOURCE)

View File

@ -1,24 +1,203 @@
# Standard library imports
from typing import Optional
import os
# PyQt imports
from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtWidgets import QDialog, QListWidgetItem
from PyQt6.QtWidgets import (
QDialog,
QListWidgetItem,
QMainWindow,
QTableWidgetItem,
)
# Third party imports
import pydymenu # type: ignore
from sqlalchemy.orm.session import Session
# App imports
from classes import MusicMusterSignals
from classes import MusicMusterSignals, TrackFileData
from config import Config
from helpers import (
ask_yes_no,
get_relative_date,
get_tags,
ms_to_mmss,
show_warning,
)
from log import log
from models import Settings, Tracks
from models import db, Settings, Tracks
from playlistmodel import PlaylistModel
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
from ui import dlg_TrackSelect_ui
from ui import dlg_replace_files_ui
class ReplaceFilesDialog(QDialog):
"""Import files as new or replacements"""
def __init__(
self,
session: Session,
main_window: QMainWindow,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.session = session
self.main_window = main_window
self.ui = dlg_replace_files_ui.Ui_Dialog()
self.ui.setupUi(self)
self.ui.lblSourceDirectory.setText(Config.REPLACE_FILES_DEFAULT_SOURCE)
self.ui.lblDestinationDirectory.setText(
Config.REPLACE_FILES_DEFAULT_DESTINATION
)
self.replacement_files: list[TrackFileData] = []
# We only want to run this against the production database because
# we will affect files in the common pool of tracks used by all
# databases
dburi = os.environ.get("ALCHEMICAL_DATABASE_URI")
if not dburi or "musicmuster_prod" not in dburi:
if not ask_yes_no(
"Not production database",
"Not on production database - continue?",
default_yes=False,
):
return
if self.ui.lblSourceDirectory.text() == self.ui.lblDestinationDirectory.text():
show_warning(
parent=self.main_window,
title="Error",
msg="Cannot import into source directory",
)
return
self.ui.tableWidget.setHorizontalHeaderLabels(["Path", "Title", "Artist"])
# Work through new files
source_dir = self.ui.lblSourceDirectory.text()
with db.Session() as session:
for new_basename in os.listdir(source_dir):
new_path = os.path.join(source_dir, new_basename)
if not os.path.isfile(new_path):
continue
rf = TrackFileData(source_file_path=new_path)
rf.tags = get_tags(new_path)
if not rf.tags['title'] or not rf.tags['artist']:
show_warning(
parent=self.main_window,
title="Error",
msg=(
f"File {new_path} missing tags\n\n:"
f"Title={rf.tags['title']}\n"
f"Artist={rf.tags['artist']}\n"
),
)
return
# Check for same filename
match_track = self.check_by_basename(
session, new_path, rf.tags['artist'], rf.tags['title']
)
if not match_track:
match_track = self.check_by_title(
session, new_path, rf.tags['artist'], rf.tags['title']
)
if not match_track:
match_track = self.get_fuzzy_match(session, new_basename)
# Build summary
rf.track_path = os.path.join(Config.REPLACE_FILES_DEFAULT_DESTINATION,
new_basename)
if match_track:
rf.track_id = match_track.id
match_basename = os.path.basename(match_track.path)
if match_basename == new_basename:
path_text = " " + new_basename + " (no change)"
else:
path_text = f" {match_basename}\n {new_basename}"
filename_item = QTableWidgetItem(path_text)
if match_track.title == rf.tags['title']:
title_text = " " + rf.tags['title'] + " (no change)"
else:
title_text = f" {match_track.title}\n {rf.tags['title']}"
title_item = QTableWidgetItem(title_text)
if match_track.artist == rf.tags['artist']:
artist_text = " " + rf.tags['artist'] + " (no change)"
else:
artist_text = f" {match_track.artist}\n {rf.tags['artist']}"
artist_item = QTableWidgetItem(artist_text)
else:
filename_item = QTableWidgetItem(" " + new_basename + " (new)")
title_item = QTableWidgetItem(" " + rf.tags['title'])
artist_item = QTableWidgetItem(" " + rf.tags['artist'])
self.replacement_files.append(rf)
row = self.ui.tableWidget.rowCount()
self.ui.tableWidget.insertRow(row)
self.ui.tableWidget.setItem(row, 0, filename_item)
self.ui.tableWidget.setItem(row, 1, title_item)
self.ui.tableWidget.setItem(row, 2, artist_item)
self.ui.tableWidget.resizeColumnsToContents()
self.ui.tableWidget.resizeRowsToContents()
def check_by_basename(
self, session: Session, new_path: str, new_path_artist: str, new_path_title: str
) -> Optional[Tracks]:
"""
Return Track that matches basename and tags
"""
match_track = None
candidates_by_basename = Tracks.get_by_basename(session, new_path)
if candidates_by_basename:
# Check tags are the same
for cbbn in candidates_by_basename:
cbbn_tags = get_tags(cbbn.path)
if (
cbbn_tags["title"].lower() == new_path_title.lower()
and cbbn_tags["artist"].lower() == new_path_artist.lower()
):
match_track = cbbn
break
return match_track
def check_by_title(
self, session: Session, new_path: str, new_path_artist: str, new_path_title: str
) -> Optional[Tracks]:
"""
Return Track that mathces title and artist
"""
match_track = None
candidates_by_title = Tracks.search_titles(session, new_path_title)
if candidates_by_title:
# Check artist tag
for cbt in candidates_by_title:
cbt_artist = get_tags(cbt.path)["artist"]
if cbt_artist.lower() == new_path_artist.lower():
match_track = cbt
break
return match_track
def get_fuzzy_match(self, session: Session, fname: str) -> Optional[Tracks]:
"""
Return Track that matches fuzzy filename search
"""
match_track = None
choice = pydymenu.rofi([a.path for a in Tracks.get_all(session)], prompt=fname)
if choice:
match_track = Tracks.get_by_path(session, choice[0])
return match_track
class TrackSelectDialog(QDialog):
@ -42,7 +221,7 @@ class TrackSelectDialog(QDialog):
self.new_row_number = new_row_number
self.source_model = source_model
self.add_to_header = add_to_header
self.ui = Ui_Dialog()
self.ui = dlg_TrackSelect_ui.Ui_Dialog()
self.ui.setupUi(self)
self.ui.btnAdd.clicked.connect(self.add_selected)
self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)

View File

@ -115,11 +115,10 @@ def get_embedded_time(text: str) -> Optional[dt.datetime]:
return None
def get_file_metadata(filepath: str) -> dict:
def get_audio_metadata(filepath: str) -> Dict[str, str | int | float]:
"""Return track metadata"""
# Get title, artist, bitrate, duration, path
metadata: Dict[str, str | int | float] = get_tags(filepath)
metadata: Dict[str, str | int | float] = {}
metadata["mtime"] = os.path.getmtime(filepath)
@ -196,7 +195,6 @@ def get_tags(path: str) -> Dict[str, Any]:
artist=tag.artist,
bitrate=round(tag.bitrate),
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
path=path,
)
@ -344,10 +342,13 @@ def send_mail(to_addr, from_addr, subj, body):
def set_track_metadata(track):
"""Set/update track metadata in database"""
metadata = get_file_metadata(track.path)
audio_metadata = get_audio_metadata(track.path)
tags = get_tags(track.path)
for key in metadata:
setattr(track, key, metadata[key])
for audio_key in audio_metadata:
setattr(track, audio_key, audio_metadata[audio_key])
for tag_key in tags:
setattr(track, tag_key, tags[tag_key])
def show_OK(parent: QMainWindow, title: str, msg: str) -> None:

View File

@ -130,7 +130,9 @@ class Playdates(dbtables.PlaydatesTable):
session.commit()
@staticmethod
def last_playdates(session: Session, track_id: int, limit=5) -> Sequence["Playdates"]:
def last_playdates(
session: Session, track_id: int, limit=5
) -> Sequence["Playdates"]:
"""
Return a list of the last limit playdates for this track, sorted
earliest to latest.
@ -169,9 +171,7 @@ class Playdates(dbtables.PlaydatesTable):
"""
return session.scalars(
Playdates.select()
.order_by(Playdates.lastplayed.desc())
.limit(limit)
Playdates.select().order_by(Playdates.lastplayed.desc()).limit(limit)
).all()
@staticmethod
@ -673,6 +673,21 @@ class Tracks(dbtables.TracksTable):
return session.scalars(select(cls)).unique().all()
@classmethod
def get_by_basename(
cls, session: Session, basename: str
) -> Optional[Sequence["Tracks"]]:
"""
Return track(s) with passed basename, or None.
"""
try:
return session.scalars(
Tracks.select().where(Tracks.path.like("%/" + basename))
).all()
except NoResultFound:
return None
@classmethod
def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]:
"""

View File

@ -7,6 +7,7 @@ from typing import cast, List, Optional
import argparse
import datetime as dt
import os
import shutil
import subprocess
import sys
import threading
@ -48,6 +49,7 @@ from PyQt6.QtWidgets import (
# Third party imports
from pygame import mixer
import pipeclient
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session
import stackprinter # type: ignore
@ -57,9 +59,10 @@ from classes import (
FadeCurve,
MusicMusterSignals,
PlaylistTrack,
TrackFileData,
)
from config import Config
from dialogs import TrackSelectDialog
from dialogs import TrackSelectDialog, ReplaceFilesDialog
from log import log
from models import db, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
from playlistmodel import PlaylistModel, PlaylistProxyModel
@ -146,12 +149,12 @@ class ImportTrack(QObject):
def __init__(
self,
filenames: List[str],
track_files: List[TrackFileData],
source_model: PlaylistModel,
row_number: Optional[int],
) -> None:
super().__init__()
self.filenames = filenames
self.track_files = track_files
self.source_model = source_model
if row_number is None:
self.next_row_number = source_model.rowCount()
@ -159,25 +162,49 @@ class ImportTrack(QObject):
self.next_row_number = row_number
self.signals = MusicMusterSignals()
# Sanity check
for tf in track_files:
if not tf.tags:
raise Exception(f"ImportTrack: no tags for {tf.source_file_path}")
if not tf.audio_metadata:
raise Exception(
f"ImportTrack: no audio_metadata for {tf.source_file_path}"
)
if tf.track_path is None:
raise Exception(f"ImportTrack: no track_path for {tf.source_file_path}")
def run(self):
"""
Create track objects from passed files and add to visible playlist
"""
with db.Session() as session:
for fname in self.filenames:
for tf in self.track_files:
self.signals.status_message_signal.emit(
f"Importing {basename(fname)}", 5000
f"Importing {basename(tf.source_file_path)}", 5000
)
metadata = helpers.get_file_metadata(fname)
# Move the track file. Check that we're not importing a
# file that's already in its final destination.
if (
os.path.exists(tf.track_path)
and os.path.exists(tf.source_file_path)
and tf.track_path != tf.source_file_path
):
os.unlink(tf.track_path)
shutil.move(tf.source_file_path, tf.track_path)
# Import track
try:
track = Tracks(session, **metadata)
track = Tracks(
session, path=tf.track_path, **tf.audio_metadata | tf.tags
)
except Exception as e:
self.signals.show_warning_signal.emit(
"Error importing track", str(e)
)
return
helpers.normalise_track(track.path)
helpers.normalise_track(tf.track_path)
# We're importing potentially multiple tracks in a loop.
# If there's an error adding the track to the Tracks
# table, the session will rollback, thus losing any
@ -187,7 +214,7 @@ class ImportTrack(QObject):
self.source_model.insert_row(self.next_row_number, track.id, "")
self.next_row_number += 1
self.signals.status_message_signal.emit(
f"{len(self.filenames)} tracks imported", 10000
f"{len(self.track_files)} tracks imported", 10000
)
self.import_finished.emit()
@ -531,6 +558,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionPaste.triggered.connect(self.paste_rows)
self.actionPlay_next.triggered.connect(self.play_next)
self.actionRenamePlaylist.triggered.connect(self.rename_playlist)
self.actionReplace_files.triggered.connect(self.replace_files)
self.actionResume.triggered.connect(self.resume)
self.actionSave_as_template.triggered.connect(self.save_as_template)
self.actionSearch_title_in_Songfacts.triggered.connect(
@ -790,56 +818,29 @@ class Window(QMainWindow, Ui_MainWindow):
return
with db.Session() as session:
new_tracks = []
for fname in dlg.selectedFiles():
txt = ""
tags = helpers.get_tags(fname)
title = tags["title"]
if not title:
helpers.show_warning(
self,
"Problem with track file",
f"{fname} does not have a title tag",
track_files: list[TrackFileData] = []
for fpath in dlg.selectedFiles():
tf = TrackFileData(fpath)
tf.tags = helpers.get_tags(fpath)
do_import = self.ok_to_import(session, fpath, tf.tags)
if do_import:
tf.track_path = os.path.join(
Config.IMPORT_DESTINATION, os.path.basename(fpath)
)
continue
artist = tags["artist"]
if not artist:
helpers.show_warning(
self,
"Problem with track file",
f"{fname} does not have an artist tag",
)
continue
count = 0
possible_matches = Tracks.search_titles(session, title)
if possible_matches:
txt += "Similar to new track "
txt += f'"{title}" by "{artist} ({fname})":\n\n'
for track in possible_matches:
txt += f' "{track.title}" by {track.artist}'
txt += f" ({track.path})\n\n"
count += 1
if count >= Config.MAX_IMPORT_MATCHES:
txt += "\nThere are more similar-looking tracks"
break
txt += "\n"
# Check whether to proceed if there were potential matches
txt += "Proceed with import?"
result = QMessageBox.question(
self,
"Possible duplicates",
txt,
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Cancel,
)
if result == QMessageBox.StandardButton.Cancel:
continue
new_tracks.append(fname)
tf.audio_metadata = helpers.get_audio_metadata(fpath)
track_files.append(tf)
self.import_filenames(track_files)
def import_filenames(self, track_files: list[TrackFileData]) -> None:
"""
Import the list of filenames as new tracks
"""
# Import in separate thread
self.import_thread = QThread()
self.worker = ImportTrack(
new_tracks,
track_files,
self.active_proxy_model(),
self.active_tab().source_model_selected_row_number(),
)
@ -850,6 +851,58 @@ class Window(QMainWindow, Ui_MainWindow):
self.import_thread.finished.connect(self.import_thread.deleteLater)
self.import_thread.start()
def ok_to_import(self, session: Session, fname: str, tags: dict[str, str]) -> bool:
"""
Check file has tags, check it's not a duplicate. Return True if this filenam
is OK to import, False if not.
"""
title = tags["title"]
if not title:
helpers.show_warning(
self,
"Problem with track file",
f"{fname} does not have a title tag",
)
return False
artist = tags["artist"]
if not artist:
helpers.show_warning(
self,
"Problem with track file",
f"{fname} does not have an artist tag",
)
return False
txt = ""
count = 0
possible_matches = Tracks.search_titles(session, title)
if possible_matches:
txt += "Similar to new track "
txt += f'"{title}" by "{artist} ({fname})":\n\n'
for track in possible_matches:
txt += f' "{track.title}" by {track.artist}'
txt += f" ({track.path})\n\n"
count += 1
if count >= Config.MAX_IMPORT_MATCHES:
txt += "\nThere are more similar-looking tracks"
break
txt += "\n"
# Check whether to proceed if there were potential matches
txt += "Proceed with import?"
result = QMessageBox.question(
self,
"Possible duplicates",
txt,
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Cancel,
)
if result == QMessageBox.StandardButton.Cancel:
return False
return True
def initialise_audacity(self) -> None:
"""
Initialise access to audacity
@ -1099,7 +1152,8 @@ class Window(QMainWindow, Ui_MainWindow):
if self.catch_return_key:
# Suppress inadvertent double press
if (
track_sequence.now.start_time and track_sequence.now.start_time
track_sequence.now.start_time
and track_sequence.now.start_time
+ dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS)
> dt.datetime.now()
):
@ -1227,6 +1281,64 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabBar.setTabText(idx, new_name)
session.commit()
def replace_files(self) -> None:
"""
Scan source directory and offer to replace existing files with "similar"
files, or import the source file as a new track.
"""
import_files: list[TrackFileData] = []
with db.Session() as session:
dlg = ReplaceFilesDialog(
session=session,
main_window=self,
)
status = dlg.exec()
if status:
for rf in dlg.replacement_files:
if rf.track_id:
# We're updating an existing track
if rf.track_path:
if os.path.exists(rf.track_path):
os.unlink(rf.track_path)
shutil.move(rf.source_file_path, rf.track_path)
track = session.get(Tracks, rf.track_id)
if not track:
raise Exception(
f"replace_files: could not retrieve track {rf.track_id}"
)
track.artist = rf.tags["artist"]
track.title = rf.tags["title"]
if track.path != rf.track_path:
track.path = rf.track_path
try:
session.commit()
except IntegrityError:
# https://jira.mariadb.org/browse/MDEV-29345 workaround
session.rollback()
track.path = "DUMMY"
session.commit()
track.path = rf.track_path
session.commit()
else:
# We're importing a new track
do_import = self.ok_to_import(
session,
os.path.basename(rf.source_file_path),
rf.tags
)
if do_import:
rf.audio_metadata = helpers.get_audio_metadata(rf.source_file_path)
import_files.append(rf)
# self.import_filenames(dlg.replacement_files)
self.import_filenames(import_files)
else:
session.rollback()
session.close()
def resume(self) -> None:
"""
Resume playing last track. We may be playing the next track

View File

@ -134,7 +134,7 @@ def main():
if process_no_matches:
prompt = f"file={new_fname}\n title={new_title}\n artist={new_artist}: "
# Use fzf to search
choice = pydymenu.fzf(parent_fnames, prompt=prompt)
choice = pydymenu.rofi(parent_fnames, prompt=prompt)
if choice:
old_file = os.path.join(parent_dir, choice[0])
oldtags = get_tags(old_file)

145
app/ui/dlgReplaceFiles.ui Normal file
View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1038</width>
<height>774</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>680</x>
<y>730</y>
<width>341</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>15</y>
<width>181</width>
<height>24</height>
</rect>
</property>
<property name="text">
<string>Source directory:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>10</x>
<y>50</y>
<width>181</width>
<height>24</height>
</rect>
</property>
<property name="text">
<string>Destination directory:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
<widget class="QLabel" name="lblSourceDirectory">
<property name="geometry">
<rect>
<x>200</x>
<y>15</y>
<width>811</width>
<height>24</height>
</rect>
</property>
<property name="text">
<string>lblSourceDirectory</string>
</property>
</widget>
<widget class="QLabel" name="lblDestinationDirectory">
<property name="geometry">
<rect>
<x>200</x>
<y>50</y>
<width>811</width>
<height>24</height>
</rect>
</property>
<property name="text">
<string>lblDestinationDirectory</string>
</property>
</widget>
<widget class="QTableWidget" name="tableWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>90</y>
<width>1001</width>
<height>621</height>
</rect>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="columnCount">
<number>3</number>
</property>
<column/>
<column/>
<column/>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,53 @@
# Form implementation generated from reading ui file 'app/ui/dlgReplaceFiles.ui'
#
# Created by: PyQt6 UI code generator 6.7.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(1038, 774)
self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog)
self.buttonBox.setGeometry(QtCore.QRect(680, 730, 341, 32))
self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setObjectName("buttonBox")
self.label = QtWidgets.QLabel(parent=Dialog)
self.label.setGeometry(QtCore.QRect(10, 15, 181, 24))
self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.label.setObjectName("label")
self.label_2 = QtWidgets.QLabel(parent=Dialog)
self.label_2.setGeometry(QtCore.QRect(10, 50, 181, 24))
self.label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.label_2.setObjectName("label_2")
self.lblSourceDirectory = QtWidgets.QLabel(parent=Dialog)
self.lblSourceDirectory.setGeometry(QtCore.QRect(200, 15, 811, 24))
self.lblSourceDirectory.setObjectName("lblSourceDirectory")
self.lblDestinationDirectory = QtWidgets.QLabel(parent=Dialog)
self.lblDestinationDirectory.setGeometry(QtCore.QRect(200, 50, 811, 24))
self.lblDestinationDirectory.setObjectName("lblDestinationDirectory")
self.tableWidget = QtWidgets.QTableWidget(parent=Dialog)
self.tableWidget.setGeometry(QtCore.QRect(20, 90, 1001, 621))
self.tableWidget.setAlternatingRowColors(True)
self.tableWidget.setColumnCount(3)
self.tableWidget.setObjectName("tableWidget")
self.tableWidget.setRowCount(0)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept) # type: ignore
self.buttonBox.rejected.connect(Dialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Source directory:"))
self.label_2.setText(_translate("Dialog", "Destination directory:"))
self.lblSourceDirectory.setText(_translate("Dialog", "lblSourceDirectory"))
self.lblDestinationDirectory.setText(_translate("Dialog", "lblDestinationDirectory"))

View File

@ -753,6 +753,8 @@ padding-left: 8px;</string>
<addaction name="actionDownload_CSV_of_played_tracks"/>
<addaction name="actionSave_as_template"/>
<addaction name="separator"/>
<addaction name="actionReplace_files"/>
<addaction name="separator"/>
<addaction name="actionE_xit"/>
</widget>
<widget class="QMenu" name="menuPlaylist">
@ -1132,6 +1134,11 @@ padding-left: 8px;</string>
<string>Select duplicate rows...</string>
</property>
</action>
<action name="actionReplace_files">
<property name="text">
<string>Replace files...</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'app/ui/main_window.ui'
#
# Created by: PyQt6 UI code generator 6.6.1
# Created by: PyQt6 UI code generator 6.7.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
@ -15,11 +15,7 @@ class Ui_MainWindow(object):
MainWindow.resize(1280, 857)
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/icons/musicmuster"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
MainWindow.setWindowIcon(icon)
MainWindow.setStyleSheet("")
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
@ -31,62 +27,39 @@ class Ui_MainWindow(object):
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.previous_track_2.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth())
self.previous_track_2.setSizePolicy(sizePolicy)
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.previous_track_2.setFont(font)
self.previous_track_2.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.previous_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.previous_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.previous_track_2.setObjectName("previous_track_2")
self.verticalLayout_3.addWidget(self.previous_track_2)
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.current_track_2.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth())
self.current_track_2.setSizePolicy(sizePolicy)
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.current_track_2.setFont(font)
self.current_track_2.setStyleSheet(
"background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.current_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.current_track_2.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);")
self.current_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.current_track_2.setObjectName("current_track_2")
self.verticalLayout_3.addWidget(self.current_track_2)
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
@ -96,29 +69,19 @@ class Ui_MainWindow(object):
font.setFamily("Sans")
font.setPointSize(20)
self.next_track_2.setFont(font)
self.next_track_2.setStyleSheet(
"background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.next_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.next_track_2.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);")
self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.next_track_2.setObjectName("next_track_2")
self.verticalLayout_3.addWidget(self.next_track_2)
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth())
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
@ -126,43 +89,32 @@ class Ui_MainWindow(object):
font.setFamily("Sans")
font.setPointSize(20)
self.hdrPreviousTrack.setFont(font)
self.hdrPreviousTrack.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.hdrPreviousTrack.setText("")
self.hdrPreviousTrack.setWordWrap(False)
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
self.verticalLayout.addWidget(self.hdrPreviousTrack)
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth())
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(20)
self.hdrCurrentTrack.setFont(font)
self.hdrCurrentTrack.setStyleSheet(
"background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;\n"
""
)
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;\n"
"")
self.hdrCurrentTrack.setText("")
self.hdrCurrentTrack.setFlat(True)
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
self.verticalLayout.addWidget(self.hdrCurrentTrack)
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
@ -170,12 +122,10 @@ class Ui_MainWindow(object):
font = QtGui.QFont()
font.setPointSize(20)
self.hdrNextTrack.setFont(font)
self.hdrNextTrack.setStyleSheet(
"background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;"
)
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;")
self.hdrNextTrack.setText("")
self.hdrNextTrack.setFlat(True)
self.hdrNextTrack.setObjectName("hdrNextTrack")
@ -210,12 +160,7 @@ class Ui_MainWindow(object):
self.cartsWidget.setObjectName("cartsWidget")
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
spacerItem = QtWidgets.QSpacerItem(
40,
20,
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Minimum,
)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_Carts.addItem(spacerItem)
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
@ -260,11 +205,7 @@ class Ui_MainWindow(object):
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
icon1 = QtGui.QIcon()
icon1.addPixmap(
QtGui.QPixmap(":/icons/headphones"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnPreview.setIcon(icon1)
self.btnPreview.setIconSize(QtCore.QSize(30, 30))
self.btnPreview.setCheckable(True)
@ -348,15 +289,10 @@ class Ui_MainWindow(object):
self.label_silent_timer.setObjectName("label_silent_timer")
self.horizontalLayout.addWidget(self.frame_silent)
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.widgetFadeVolume.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
self.widgetFadeVolume.setSizePolicy(sizePolicy)
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
@ -373,11 +309,7 @@ class Ui_MainWindow(object):
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon2 = QtGui.QIcon()
icon2.addPixmap(
QtGui.QPixmap(":/icons/fade"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon2.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnFade.setIcon(icon2)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
@ -385,11 +317,7 @@ class Ui_MainWindow(object):
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
icon3 = QtGui.QIcon()
icon3.addPixmap(
QtGui.QPixmap(":/icons/stopsign"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon3.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnStop.setIcon(icon3)
self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop)
@ -415,69 +343,39 @@ class Ui_MainWindow(object):
MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon4 = QtGui.QIcon()
icon4.addPixmap(
QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon4.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionPlay_next.setIcon(icon4)
self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon5 = QtGui.QIcon()
icon5.addPixmap(
QtGui.QPixmap(":/icons/next"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon5.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionSkipToNext.setIcon(icon5)
self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon()
icon6.addPixmap(
QtGui.QPixmap(
"app/ui/../../../../.designer/backup/icon_search_database.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionInsertTrack.setIcon(icon6)
self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon()
icon7.addPixmap(
QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionAdd_file.setIcon(icon7)
self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon()
icon8.addPixmap(
QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionFade.setIcon(icon8)
self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon()
icon9.addPixmap(
QtGui.QPixmap(":/icons/stop"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon9.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionStop.setIcon(icon9)
self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon10 = QtGui.QIcon()
icon10.addPixmap(
QtGui.QPixmap(":/icons/previous"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon10.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.action_Resume_previous.setIcon(icon10)
self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow)
@ -524,9 +422,7 @@ class Ui_MainWindow(object):
self.actionImport = QtGui.QAction(parent=MainWindow)
self.actionImport.setObjectName("actionImport")
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionDownload_CSV_of_played_tracks.setObjectName(
"actionDownload_CSV_of_played_tracks"
)
self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks")
self.actionSearch = QtGui.QAction(parent=MainWindow)
self.actionSearch.setObjectName("actionSearch")
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
@ -554,15 +450,13 @@ class Ui_MainWindow(object):
self.actionResume = QtGui.QAction(parent=MainWindow)
self.actionResume.setObjectName("actionResume")
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Wikipedia.setObjectName(
"actionSearch_title_in_Wikipedia"
)
self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia")
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Songfacts.setObjectName(
"actionSearch_title_in_Songfacts"
)
self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts")
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
self.actionReplace_files = QtGui.QAction(parent=MainWindow)
self.actionReplace_files.setObjectName("actionReplace_files")
self.menuFile.addAction(self.actionNewPlaylist)
self.menuFile.addAction(self.actionNew_from_template)
self.menuFile.addAction(self.actionOpenPlaylist)
@ -578,6 +472,8 @@ class Ui_MainWindow(object):
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
self.menuFile.addAction(self.actionSave_as_template)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionReplace_files)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionE_xit)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionPlay_next)
@ -611,7 +507,7 @@ class Ui_MainWindow(object):
self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1)
self.tabInfolist.setCurrentIndex(-1)
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
@ -647,58 +543,38 @@ class Ui_MainWindow(object):
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
self.actionStop.setText(_translate("MainWindow", "S&top"))
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
self.action_Clear_selection.setText(
_translate("MainWindow", "Clear &selection")
)
self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection"))
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
self.action_Resume_previous.setText(
_translate("MainWindow", "&Resume previous")
)
self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous"))
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
self.actionTest.setText(_translate("MainWindow", "&Test"))
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
self.actionSkipToFade.setText(
_translate("MainWindow", "&Skip to start of fade")
)
self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade"))
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
self.actionMoveSelected.setText(
_translate("MainWindow", "Mo&ve selected tracks to...")
)
self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to..."))
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.actionSelect_next_track.setText(
_translate("MainWindow", "Select next track")
)
self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track"))
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
self.actionSelect_previous_track.setText(
_translate("MainWindow", "Select previous track")
)
self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track"))
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(
_translate("MainWindow", "Select played tracks")
)
self.actionMoveUnplayed.setText(
_translate("MainWindow", "Move &unplayed tracks to...")
)
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
self.actionImport.setText(_translate("MainWindow", "Import track..."))
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
self.actionDownload_CSV_of_played_tracks.setText(
_translate("MainWindow", "Download CSV of played tracks...")
)
self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks..."))
self.actionSearch.setText(_translate("MainWindow", "Search..."))
self.actionSearch.setShortcut(_translate("MainWindow", "/"))
self.actionInsertSectionHeader.setText(
_translate("MainWindow", "Insert &section header...")
)
self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert &section header..."))
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
self.actionFind_next.setText(_translate("MainWindow", "Find next"))
@ -706,12 +582,8 @@ class Ui_MainWindow(object):
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
self.action_About.setText(_translate("MainWindow", "&About"))
self.actionSave_as_template.setText(
_translate("MainWindow", "Save as template...")
)
self.actionNew_from_template.setText(
_translate("MainWindow", "New from template...")
)
self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
self.actionNew_from_template.setText(_translate("MainWindow", "New from template..."))
self.actionDebug.setText(_translate("MainWindow", "Debug"))
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
@ -720,22 +592,11 @@ class Ui_MainWindow(object):
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
self.actionSearch_title_in_Wikipedia.setText(
_translate("MainWindow", "Search title in Wikipedia")
)
self.actionSearch_title_in_Wikipedia.setShortcut(
_translate("MainWindow", "Ctrl+W")
)
self.actionSearch_title_in_Songfacts.setText(
_translate("MainWindow", "Search title in Songfacts")
)
self.actionSearch_title_in_Songfacts.setShortcut(
_translate("MainWindow", "Ctrl+S")
)
self.actionSelect_duplicate_rows.setText(
_translate("MainWindow", "Select duplicate rows...")
)
self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia"))
self.actionSearch_title_in_Wikipedia.setShortcut(_translate("MainWindow", "Ctrl+W"))
self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts"))
self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S"))
self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows..."))
self.actionReplace_files.setText(_translate("MainWindow", "Replace files..."))
from infotabs import InfoTabs
from pyqtgraph import PlotWidget

View File

@ -33,11 +33,11 @@ class TestMMModels(unittest.TestCase):
with db.Session() as session:
track1_path = "testdata/isa.mp3"
metadata1 = helpers.get_file_metadata(track1_path)
metadata1 = helpers.get_audio_metadata(track1_path)
self.track1 = Tracks(session, **metadata1)
track2_path = "testdata/mom.mp3"
metadata2 = helpers.get_file_metadata(track2_path)
metadata2 = helpers.get_audio_metadata(track2_path)
self.track2 = Tracks(session, **metadata2)
def tearDown(self):

View File

@ -11,7 +11,7 @@ from sqlalchemy.orm.session import Session
# App imports
from app.log import log
from app.helpers import get_file_metadata
from app.helpers import get_audio_metadata
# Set up test database before importing db
# Mark subsequent lines to ignore E402, imports not at top of file
@ -51,7 +51,7 @@ class TestMMMiscTracks(unittest.TestCase):
for row in range(len(self.test_tracks)):
track_path = self.test_tracks[row % len(self.test_tracks)]
metadata = get_file_metadata(track_path)
metadata = get_audio_metadata(track_path)
track = Tracks(session, **metadata)
self.model.insert_row(
proposed_row_number=row, track_id=track.id, note=f"{row=}"
@ -113,7 +113,7 @@ class TestMMMiscNoPlaylist(unittest.TestCase):
_ = str(model)
track_path = self.test_tracks[0]
metadata = get_file_metadata(track_path)
metadata = get_audio_metadata(track_path)
track = Tracks(session, **metadata)
model.insert_row(proposed_row_number=0, track_id=track.id)