Compare commits

..

7 Commits

Author SHA1 Message Date
Keith Edmunds
d050fa0d84 Fix file importing
Imported track wasn't moved to destination
2024-05-06 12:12:56 +01:00
Keith Edmunds
e25d4ad311 Fixup tests 2024-05-05 18:38:50 +01:00
Keith Edmunds
c1d2fcd8cd Save open tabs properly
Fixes #239
2024-05-04 21:15:08 +01:00
Keith Edmunds
253550b490 Implement SQLAlchemy Pool.pre_ping
Fixes #241
2024-05-04 20:35:14 +01:00
Keith Edmunds
9fb7cce82c Update dependencies 2024-05-04 02:08:18 +01:00
Keith Edmunds
f2db9967fb Reduce stdout output 2024-05-04 02:08:00 +01:00
Keith Edmunds
a24ff76b6b Build in replace_file functionality
Major rewrite of file importing

Fixes #141
2024-05-03 22:40:21 +01:00
17 changed files with 766 additions and 484 deletions

View File

@ -1,6 +1,6 @@
# Standard library imports # Standard library imports
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Optional from typing import Any, Optional
import datetime as dt import datetime as dt
# PyQt imports # PyQt imports
@ -203,6 +203,19 @@ class PlaylistTrack:
self.fade_graph_start_updates = now + dt.timedelta(milliseconds=update_graph_at_ms) 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): class AddFadeCurve(QObject):
""" """
Initialising a fade curve introduces a noticeable delay so carry out in Initialising a fade curve introduces a noticeable delay so carry out in

View File

@ -39,6 +39,7 @@ class Config(object):
DEFAULT_COLUMN_WIDTH = 200 DEFAULT_COLUMN_WIDTH = 200
DISPLAY_SQL = False DISPLAY_SQL = False
EPOCH = dt.datetime(1970, 1, 1) EPOCH = dt.datetime(1970, 1, 1)
ENGINE_OPTIONS = dict(pool_pre_ping=True)
ERRORS_FROM = ["noreply@midnighthax.com"] ERRORS_FROM = ["noreply@midnighthax.com"]
ERRORS_TO = ["kae@midnighthax.com"] ERRORS_TO = ["kae@midnighthax.com"]
FADE_CURVE_BACKGROUND = "lightyellow" FADE_CURVE_BACKGROUND = "lightyellow"
@ -78,6 +79,7 @@ class Config(object):
OBS_PASSWORD = "auster" OBS_PASSWORD = "auster"
OBS_PORT = 4455 OBS_PORT = 4455
PLAY_SETTLE = 500000 PLAY_SETTLE = 500000
REPLACE_FILES_DEFAULT_SOURCE = "/home/kae/music/Singles/tmp"
RETURN_KEY_DEBOUNCE_MS = 500 RETURN_KEY_DEBOUNCE_MS = 500
ROOT = os.environ.get("ROOT") or "/home/kae/music" ROOT = os.environ.get("ROOT") or "/home/kae/music"
ROWS_FROM_ZERO = True ROWS_FROM_ZERO = True
@ -94,3 +96,4 @@ class Config(object):
# These rely on earlier definitions # These rely on earlier definitions
IMPORT_DESTINATION = os.path.join(ROOT, "Singles") 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 # Standard library imports
from typing import Optional from typing import Optional
import os
# PyQt imports # PyQt imports
from PyQt6.QtCore import QEvent, Qt from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtWidgets import QDialog, QListWidgetItem from PyQt6.QtWidgets import (
QDialog,
QListWidgetItem,
QMainWindow,
QTableWidgetItem,
)
# Third party imports # Third party imports
import pydymenu # type: ignore
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
# App imports # App imports
from classes import MusicMusterSignals from classes import MusicMusterSignals, TrackFileData
from config import Config
from helpers import ( from helpers import (
ask_yes_no, ask_yes_no,
get_relative_date, get_relative_date,
get_tags,
ms_to_mmss, ms_to_mmss,
show_warning,
) )
from log import log from log import log
from models import Settings, Tracks from models import db, Settings, Tracks
from playlistmodel import PlaylistModel 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): class TrackSelectDialog(QDialog):
@ -42,7 +221,7 @@ class TrackSelectDialog(QDialog):
self.new_row_number = new_row_number self.new_row_number = new_row_number
self.source_model = source_model self.source_model = source_model
self.add_to_header = add_to_header self.add_to_header = add_to_header
self.ui = Ui_Dialog() self.ui = dlg_TrackSelect_ui.Ui_Dialog()
self.ui.setupUi(self) self.ui.setupUi(self)
self.ui.btnAdd.clicked.connect(self.add_selected) self.ui.btnAdd.clicked.connect(self.add_selected)
self.ui.btnAddClose.clicked.connect(self.add_selected_and_close) self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)

View File

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

View File

@ -33,7 +33,7 @@ if ALCHEMICAL_DATABASE_URI is None:
raise ValueError("ALCHEMICAL_DATABASE_URI is undefined") raise ValueError("ALCHEMICAL_DATABASE_URI is undefined")
if "unittest" in sys.modules and "sqlite" not in ALCHEMICAL_DATABASE_URI: if "unittest" in sys.modules and "sqlite" not in ALCHEMICAL_DATABASE_URI:
raise ValueError("Unit tests running on non-Sqlite database") raise ValueError("Unit tests running on non-Sqlite database")
db = Alchemical(ALCHEMICAL_DATABASE_URI) db = Alchemical(ALCHEMICAL_DATABASE_URI, engine_options=Config.ENGINE_OPTIONS)
# Database classes # Database classes
@ -130,7 +130,9 @@ class Playdates(dbtables.PlaydatesTable):
session.commit() session.commit()
@staticmethod @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 Return a list of the last limit playdates for this track, sorted
earliest to latest. earliest to latest.
@ -169,9 +171,7 @@ class Playdates(dbtables.PlaydatesTable):
""" """
return session.scalars( return session.scalars(
Playdates.select() Playdates.select().order_by(Playdates.lastplayed.desc()).limit(limit)
.order_by(Playdates.lastplayed.desc())
.limit(limit)
).all() ).all()
@staticmethod @staticmethod
@ -673,6 +673,21 @@ class Tracks(dbtables.TracksTable):
return session.scalars(select(cls)).unique().all() 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 @classmethod
def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]: 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 argparse
import datetime as dt import datetime as dt
import os import os
import shutil
import subprocess import subprocess
import sys import sys
import threading import threading
@ -48,6 +49,7 @@ from PyQt6.QtWidgets import (
# Third party imports # Third party imports
from pygame import mixer from pygame import mixer
import pipeclient import pipeclient
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
import stackprinter # type: ignore import stackprinter # type: ignore
@ -57,9 +59,10 @@ from classes import (
FadeCurve, FadeCurve,
MusicMusterSignals, MusicMusterSignals,
PlaylistTrack, PlaylistTrack,
TrackFileData,
) )
from config import Config from config import Config
from dialogs import TrackSelectDialog from dialogs import TrackSelectDialog, ReplaceFilesDialog
from log import log from log import log
from models import db, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks from models import db, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
@ -146,12 +149,12 @@ class ImportTrack(QObject):
def __init__( def __init__(
self, self,
filenames: List[str], track_files: List[TrackFileData],
source_model: PlaylistModel, source_model: PlaylistModel,
row_number: Optional[int], row_number: Optional[int],
) -> None: ) -> None:
super().__init__() super().__init__()
self.filenames = filenames self.track_files = track_files
self.source_model = source_model self.source_model = source_model
if row_number is None: if row_number is None:
self.next_row_number = source_model.rowCount() self.next_row_number = source_model.rowCount()
@ -159,25 +162,50 @@ class ImportTrack(QObject):
self.next_row_number = row_number self.next_row_number = row_number
self.signals = MusicMusterSignals() 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): def run(self):
""" """
Create track objects from passed files and add to visible playlist Create track objects from passed files and add to visible playlist
""" """
with db.Session() as session: with db.Session() as session:
for fname in self.filenames: for tf in self.track_files:
self.signals.status_message_signal.emit( 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)
# Sanity check
if not os.path.exists(tf.source_file_path):
log.error(f"ImportTrack: file not found: {tf.source_file_path=}")
continue
# 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 tf.track_path != tf.source_file_path:
os.unlink(tf.track_path)
shutil.move(tf.source_file_path, tf.track_path)
# Import track
try: try:
track = Tracks(session, **metadata) track = Tracks(
session, path=tf.track_path, **tf.audio_metadata | tf.tags
)
except Exception as e: except Exception as e:
self.signals.show_warning_signal.emit( self.signals.show_warning_signal.emit(
"Error importing track", str(e) "Error importing track", str(e)
) )
return return
helpers.normalise_track(track.path) helpers.normalise_track(tf.track_path)
# We're importing potentially multiple tracks in a loop. # We're importing potentially multiple tracks in a loop.
# If there's an error adding the track to the Tracks # If there's an error adding the track to the Tracks
# table, the session will rollback, thus losing any # table, the session will rollback, thus losing any
@ -187,7 +215,7 @@ class ImportTrack(QObject):
self.source_model.insert_row(self.next_row_number, track.id, "") self.source_model.insert_row(self.next_row_number, track.id, "")
self.next_row_number += 1 self.next_row_number += 1
self.signals.status_message_signal.emit( 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() self.import_finished.emit()
@ -459,10 +487,14 @@ class Window(QMainWindow, Ui_MainWindow):
record.update(session, {"f_int": splitter_bottom}) record.update(session, {"f_int": splitter_bottom})
# Save tab number of open playlists # Save tab number of open playlists
open_playlist_ids: dict[int, int] = {}
for idx in range(self.tabPlaylist.count()): for idx in range(self.tabPlaylist.count()):
playlist_id = self.tabPlaylist.widget(idx).playlist_id open_playlist_ids[self.tabPlaylist.widget(idx).playlist_id] = idx
Playlists.clear_tabs(session, list(open_playlist_ids.keys()))
for playlist_id, idx in open_playlist_ids.items():
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if playlist: if playlist:
log.debug(f"Set {playlist=} tab to {idx=}")
playlist.tab = idx playlist.tab = idx
session.flush() session.flush()
@ -531,6 +563,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionPaste.triggered.connect(self.paste_rows) self.actionPaste.triggered.connect(self.paste_rows)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
self.actionRenamePlaylist.triggered.connect(self.rename_playlist) self.actionRenamePlaylist.triggered.connect(self.rename_playlist)
self.actionReplace_files.triggered.connect(self.replace_files)
self.actionResume.triggered.connect(self.resume) self.actionResume.triggered.connect(self.resume)
self.actionSave_as_template.triggered.connect(self.save_as_template) self.actionSave_as_template.triggered.connect(self.save_as_template)
self.actionSearch_title_in_Songfacts.triggered.connect( self.actionSearch_title_in_Songfacts.triggered.connect(
@ -603,7 +636,7 @@ class Window(QMainWindow, Ui_MainWindow):
add tab to display. Return index number of tab. add tab to display. Return index number of tab.
""" """
log.info(f"create_playlist_tab({playlist=})") log.debug(f"create_playlist_tab({playlist=})")
playlist_tab = PlaylistTab( playlist_tab = PlaylistTab(
musicmuster=self, musicmuster=self,
@ -611,7 +644,7 @@ class Window(QMainWindow, Ui_MainWindow):
) )
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
log.info(f"create_playlist_tab() returned: {idx=}") log.debug(f"create_playlist_tab() returned: {idx=}")
return idx return idx
def cut_rows(self) -> None: def cut_rows(self) -> None:
@ -790,56 +823,29 @@ class Window(QMainWindow, Ui_MainWindow):
return return
with db.Session() as session: with db.Session() as session:
new_tracks = [] track_files: list[TrackFileData] = []
for fname in dlg.selectedFiles(): for fpath in dlg.selectedFiles():
txt = "" tf = TrackFileData(fpath)
tags = helpers.get_tags(fname) tf.tags = helpers.get_tags(fpath)
title = tags["title"] do_import = self.ok_to_import(session, fpath, tf.tags)
if not title: if do_import:
helpers.show_warning( tf.track_path = os.path.join(
self, Config.IMPORT_DESTINATION, os.path.basename(fpath)
"Problem with track file",
f"{fname} does not have a title tag",
) )
continue tf.audio_metadata = helpers.get_audio_metadata(fpath)
artist = tags["artist"] track_files.append(tf)
if not artist:
helpers.show_warning( self.import_filenames(track_files)
self,
"Problem with track file", def import_filenames(self, track_files: list[TrackFileData]) -> None:
f"{fname} does not have an artist tag", """
) Import the list of filenames as new tracks
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)
# Import in separate thread # Import in separate thread
self.import_thread = QThread() self.import_thread = QThread()
self.worker = ImportTrack( self.worker = ImportTrack(
new_tracks, track_files,
self.active_proxy_model(), self.active_proxy_model(),
self.active_tab().source_model_selected_row_number(), self.active_tab().source_model_selected_row_number(),
) )
@ -850,6 +856,58 @@ class Window(QMainWindow, Ui_MainWindow):
self.import_thread.finished.connect(self.import_thread.deleteLater) self.import_thread.finished.connect(self.import_thread.deleteLater)
self.import_thread.start() 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: def initialise_audacity(self) -> None:
""" """
Initialise access to audacity Initialise access to audacity
@ -857,7 +915,7 @@ class Window(QMainWindow, Ui_MainWindow):
try: try:
self.audacity_client = pipeclient.PipeClient() self.audacity_client = pipeclient.PipeClient()
log.info(f"{hex(id(self.audacity_client))=}") log.debug(f"{hex(id(self.audacity_client))=}")
except RuntimeError as e: except RuntimeError as e:
log.error(f"Unable to initialise Audacity: {str(e)}") log.error(f"Unable to initialise Audacity: {str(e)}")
@ -1099,7 +1157,8 @@ class Window(QMainWindow, Ui_MainWindow):
if self.catch_return_key: if self.catch_return_key:
# Suppress inadvertent double press # Suppress inadvertent double press
if ( 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.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS)
> dt.datetime.now() > dt.datetime.now()
): ):
@ -1227,6 +1286,64 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabBar.setTabText(idx, new_name) self.tabBar.setTabText(idx, new_name)
session.commit() 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: def resume(self) -> None:
""" """
Resume playing last track. We may be playing the next track Resume playing last track. We may be playing the next track

View File

@ -172,7 +172,7 @@ class PlaylistTab(QTableView):
# Save passed settings # Save passed settings
self.musicmuster = musicmuster self.musicmuster = musicmuster
self.playlist_id = playlist_id self.playlist_id = playlist_id
log.info(f"PlaylistTab.__init__({playlist_id=})") log.debug(f"PlaylistTab.__init__({playlist_id=})")
# Set up widget # Set up widget
self.source_model = PlaylistModel(playlist_id) self.source_model = PlaylistModel(playlist_id)
@ -531,7 +531,7 @@ class PlaylistTab(QTableView):
Called when column width changes. Save new width to database. Called when column width changes. Save new width to database.
""" """
log.info(f"_column_resize({column_number=}, {_old=}, {_new=}") log.debug(f"_column_resize({column_number=}, {_old=}, {_new=}")
header = self.horizontalHeader() header = self.horizontalHeader()
if not header: if not header:
@ -635,7 +635,7 @@ class PlaylistTab(QTableView):
) )
) )
log.info(f"get_selected_rows() returned: {result=}") log.debug(f"get_selected_rows() returned: {result=}")
return result return result
def _import_from_audacity(self, row_number: int) -> None: def _import_from_audacity(self, row_number: int) -> None:
@ -726,7 +726,7 @@ class PlaylistTab(QTableView):
If playlist_id is us, resize rows If playlist_id is us, resize rows
""" """
log.info(f"resize_rows({playlist_id=}) {self.playlist_id=}") log.debug(f"resize_rows({playlist_id=}) {self.playlist_id=}")
if playlist_id and playlist_id != self.playlist_id: if playlist_id and playlist_id != self.playlist_id:
return return
@ -828,7 +828,7 @@ class PlaylistTab(QTableView):
def _set_column_widths(self) -> None: def _set_column_widths(self) -> None:
"""Column widths from settings""" """Column widths from settings"""
log.info("_set_column_widths()") log.debug("_set_column_widths()")
header = self.horizontalHeader() header = self.horizontalHeader()
if not header: if not header:

View File

@ -134,7 +134,7 @@ def main():
if process_no_matches: if process_no_matches:
prompt = f"file={new_fname}\n title={new_title}\n artist={new_artist}: " prompt = f"file={new_fname}\n title={new_title}\n artist={new_artist}: "
# Use fzf to search # Use fzf to search
choice = pydymenu.fzf(parent_fnames, prompt=prompt) choice = pydymenu.rofi(parent_fnames, prompt=prompt)
if choice: if choice:
old_file = os.path.join(parent_dir, choice[0]) old_file = os.path.join(parent_dir, choice[0])
oldtags = get_tags(old_file) 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="actionDownload_CSV_of_played_tracks"/>
<addaction name="actionSave_as_template"/> <addaction name="actionSave_as_template"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionReplace_files"/>
<addaction name="separator"/>
<addaction name="actionE_xit"/> <addaction name="actionE_xit"/>
</widget> </widget>
<widget class="QMenu" name="menuPlaylist"> <widget class="QMenu" name="menuPlaylist">
@ -1132,6 +1134,11 @@ padding-left: 8px;</string>
<string>Select duplicate rows...</string> <string>Select duplicate rows...</string>
</property> </property>
</action> </action>
<action name="actionReplace_files">
<property name="text">
<string>Replace files...</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

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

233
poetry.lock generated
View File

@ -79,27 +79,6 @@ files = [
[package.extras] [package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "beautifulsoup4"
version = "4.12.3"
description = "Screen-scraping library"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
]
[package.dependencies]
soupsieve = ">1.2"
[package.extras]
cchardet = ["cchardet"]
chardet = ["chardet"]
charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]] [[package]]
name = "black" name = "black"
version = "24.4.2" version = "24.4.2"
@ -460,23 +439,6 @@ urllib3 = "*"
dev = ["dlint", "flake8-2020", "flake8-aaa", "flake8-absolute-import", "flake8-alfred", "flake8-annotations-complexity", "flake8-bandit", "flake8-black", "flake8-broken-line", "flake8-bugbear", "flake8-builtins", "flake8-coding", "flake8-cognitive-complexity", "flake8-commas", "flake8-comprehensions", "flake8-debugger", "flake8-django", "flake8-docstrings", "flake8-eradicate", "flake8-executable", "flake8-expression-complexity", "flake8-fixme", "flake8-functions", "flake8-future-import", "flake8-import-order", "flake8-isort", "flake8-logging-format", "flake8-mock", "flake8-mutable", "flake8-mypy", "flake8-pep3101", "flake8-pie", "flake8-print", "flake8-printf-formatting", "flake8-pyi", "flake8-pytest", "flake8-pytest-style", "flake8-quotes", "flake8-requirements", "flake8-rst-docstrings", "flake8-scrapy", "flake8-spellcheck", "flake8-sql", "flake8-strict", "flake8-string-format", "flake8-tidy-imports", "flake8-todo", "flake8-use-fstring", "flake8-variables-names", "isort[pyproject]", "mccabe", "pandas-vet", "pep8-naming", "pylint", "pytest", "typing-extensions", "wemake-python-styleguide"] dev = ["dlint", "flake8-2020", "flake8-aaa", "flake8-absolute-import", "flake8-alfred", "flake8-annotations-complexity", "flake8-bandit", "flake8-black", "flake8-broken-line", "flake8-bugbear", "flake8-builtins", "flake8-coding", "flake8-cognitive-complexity", "flake8-commas", "flake8-comprehensions", "flake8-debugger", "flake8-django", "flake8-docstrings", "flake8-eradicate", "flake8-executable", "flake8-expression-complexity", "flake8-fixme", "flake8-functions", "flake8-future-import", "flake8-import-order", "flake8-isort", "flake8-logging-format", "flake8-mock", "flake8-mutable", "flake8-mypy", "flake8-pep3101", "flake8-pie", "flake8-print", "flake8-printf-formatting", "flake8-pyi", "flake8-pytest", "flake8-pytest-style", "flake8-quotes", "flake8-requirements", "flake8-rst-docstrings", "flake8-scrapy", "flake8-spellcheck", "flake8-sql", "flake8-strict", "flake8-string-format", "flake8-tidy-imports", "flake8-todo", "flake8-use-fstring", "flake8-variables-names", "isort[pyproject]", "mccabe", "pandas-vet", "pep8-naming", "pylint", "pytest", "typing-extensions", "wemake-python-styleguide"]
docs = ["alabaster", "pygments-github-lexers", "recommonmark", "sphinx"] docs = ["alabaster", "pygments-github-lexers", "recommonmark", "sphinx"]
[[package]]
name = "furo"
version = "2023.9.10"
description = "A clean customisable Sphinx documentation theme."
optional = false
python-versions = ">=3.8"
files = [
{file = "furo-2023.9.10-py3-none-any.whl", hash = "sha256:513092538537dc5c596691da06e3c370714ec99bc438680edc1debffb73e5bfc"},
{file = "furo-2023.9.10.tar.gz", hash = "sha256:5707530a476d2a63b8cad83b4f961f3739a69f4b058bcf38a03a39fa537195b2"},
]
[package.dependencies]
beautifulsoup4 = "*"
pygments = ">=2.7"
sphinx = ">=6.0,<8.0"
sphinx-basic-ng = "*"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.0.3" version = "3.0.3"
@ -671,75 +633,75 @@ i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "line-profiler" name = "line-profiler"
version = "4.1.2" version = "4.1.3"
description = "Line-by-line profiler" description = "Line-by-line profiler"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "line_profiler-4.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4344c1504ad1a57029a8ab30812d967a0917cad7b654077e8787e4a7d7ea3469"}, {file = "line_profiler-4.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b26cccca30c0f859c585cd4a6c75ffde4dca80ba98a858d3d04b44a6b560c65"},
{file = "line_profiler-4.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0720b356db3e9ca297c3260f280c5be3bb4b230eda61ce73b4df5e553418d37a"}, {file = "line_profiler-4.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e8a1ed7bf88049cb8d069a2dac96c91b25b5a77cb712c207b7f484ab86f8b134"},
{file = "line_profiler-4.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09f742af37166768f92495bd3d3a71da1ba41d3004307a66c108f29ed947d6e1"}, {file = "line_profiler-4.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320a8ccb2b9d0df85b8f19000242407d0cb1ea5804b4967fe6f755824c81a87"},
{file = "line_profiler-4.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:443a5df10eb7910df694340c8a81c1668a88bb59ca44149a3291f7b2ae960891"}, {file = "line_profiler-4.1.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5751939d9dd95b1ec74e0aee428fe17d037fcb346fd23a7bf928b71c2dca2d19"},
{file = "line_profiler-4.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a906f9d1687eea7e5b22e3bd367d4b63706fcea1906baaad76b1cc4c1142553d"}, {file = "line_profiler-4.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b45f405d63730e5284403c1ff293f1e7f8ac7a39486db4c55a858712cec333d"},
{file = "line_profiler-4.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3b2c8cc34a776c5cfaa4a4a09a51541efcc9082dce15b19e494000e82576ced"}, {file = "line_profiler-4.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9e24d61810ad153ab6a795d68f735812de4131f282128b799467f7fa56cac94f"},
{file = "line_profiler-4.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:55ca0a78eb8d52515486c374ec53fa9e65e3c4128e8bbc909d8bfce267a91fdd"}, {file = "line_profiler-4.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f961465381e5bdc9fa7e5597af6714ada700d3e6ca61cca56763477f1047ff23"},
{file = "line_profiler-4.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:f4a11389f06831d7984b63be0743fbbbae1ffb56fad04b4e538d3e6933b5c265"}, {file = "line_profiler-4.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:6112436cb48ab635bc64e3dbfd80f67b56967e72aa7853e5084a64e11be5fe65"},
{file = "line_profiler-4.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:32fa07f6fecfd209329559e4ae945dc7bdc0703355c8924bbf19101495b2373f"}, {file = "line_profiler-4.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:16c8d2830e9daf0bcd49422e9367db5c825b02b88c383b9228c281ce14a5ad80"},
{file = "line_profiler-4.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f8e9e8af6660629f214e424613c56a6622cf36d9c638c569c926b21374d7029"}, {file = "line_profiler-4.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e3ed5dd55bda1b0f65893ff377b6aedae69490f7be4fd5d818dd5bcc75553bf"},
{file = "line_profiler-4.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4753113c4e2c30a547937dbc456900d7f3a1b99bc8bc81a640a89306cd729c0f"}, {file = "line_profiler-4.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0ad37589b270e59f65ec6704435f02ece6d4246af112c0413095a5d3b13285b"},
{file = "line_profiler-4.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f0989302404850a2a041ba60afe6c7240aea10fdd9432d5c1d464aca39a0369"}, {file = "line_profiler-4.1.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c29ef65e3e0085f20ffedcddfa8d02f6f6eaa0dacec29129cd74d206f9f6c"},
{file = "line_profiler-4.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4b25ee412b0cd624614edd16c4c0af02dbeb73db2a08a49a14b120005a5630"}, {file = "line_profiler-4.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ef054e1b6fd2443341911a2ddad0f8b6ed24903fa6a7e5e8201cd4272132e3a"},
{file = "line_profiler-4.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93c6a49009ee75dcd8ff644c5fd39eeb8bb672d5a41bacdd239db14ae1ba3098"}, {file = "line_profiler-4.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:02bc0650ef8f87a489d6fbafcc0040ca76144d2a4c40e4044babccfe769b5525"},
{file = "line_profiler-4.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b96964cdb306741a01b95d210d634cc79ed70d2904336cbd8f69a9b5f284426d"}, {file = "line_profiler-4.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f032c0973f0c1150440dce5f9b91509fce474c11b10c2c93a2109e1e0dab8a45"},
{file = "line_profiler-4.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:46a8cad2cb4b6a1229ddccf06694b1d01fd5acd1cf8c502caf937765a7c877de"}, {file = "line_profiler-4.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:ec8a34285338aadc6a74e91b022b6d8ea19ac5deaaa0c9b880a1ab7b4ed45c43"},
{file = "line_profiler-4.1.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a102fd8e13abd367379e39fd9426fd60e1e3a39fcd80fa25641618969464c022"}, {file = "line_profiler-4.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8ae10578f1325772ccfa2833288d826e4bc781214d74b87331a6b7e5793252ca"},
{file = "line_profiler-4.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44ee51bce974d6b2269492299d4abae6db1b06ae7617760c7436c597dbdbd032"}, {file = "line_profiler-4.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b7c89c68379879d3a11c5e76499f0f7a08683436762af6bf51db126d3cb9cdd9"},
{file = "line_profiler-4.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e4cafd9a1effe1b9646f6a86716dbd291684fde1f8a297930d845d8a9340299"}, {file = "line_profiler-4.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9f4abf9ecb8b508d96420dde44d54a8484e73468132229bbba2229283a7e9fb"},
{file = "line_profiler-4.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b433a2918e522d6dd0e6bdcf1216cede15c4f201f7eeb0d816114fbac5031cd7"}, {file = "line_profiler-4.1.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d12bf40ed654ad1d5c132be172054b9ec5ae3ba138ca2099002075fb14396a64"},
{file = "line_profiler-4.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad96accb1f5cdedfe2e6607f9be86d28196d3f743229e2b67bd28a40f76f133"}, {file = "line_profiler-4.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d17f3bf22b9c7d72b3cb2d283d71152f4cc98e8ba88e720c743b2e3d9be6ad"},
{file = "line_profiler-4.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4eb9df035861f7c2e9852773dff72a3324e2e5aebc0b8c7c2ba22437387ef5e7"}, {file = "line_profiler-4.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9d7c7593ae86215d99d1d32e4b92ed6ace2ac8388aab781b74bf97d44e72ff1f"},
{file = "line_profiler-4.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e733c0e6626d0e9f1b434da40b93ed1c00ea503f3ced04f5a58c22d1163fe1c1"}, {file = "line_profiler-4.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:248f16ba356ac1e19be834b0bdaf29c95c1c9229beaa63e0e3aad9aa3edfc012"},
{file = "line_profiler-4.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:8cc0c24384e29e99da5627669dbf312a23d11138de0169aa58d4ea5187522ba0"}, {file = "line_profiler-4.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:b85468d30ed16e362e8a044df0f331796c6ec5a76a55e88aae57078a2eec6afa"},
{file = "line_profiler-4.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:900ad7be6d609fb1442200c7757de3534b381d6eeac22fa0135c5d0a900b5787"}, {file = "line_profiler-4.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:82d5333d1ffac08b34828213bd674165e50876610061faa97660928b346a620d"},
{file = "line_profiler-4.1.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49c6c6e19c3c0d7cc8f1641ece9e52fec5e99c56472e26156c16473b7568d374"}, {file = "line_profiler-4.1.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f56985a885e2936eab6303fc82f1a20e5e0bb6d4d8f44f8a3825179d261053e"},
{file = "line_profiler-4.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7ed1edd85f9a005a3e1316b3962a5fc42a159257cf2dfd13d10fcbefaece8ce"}, {file = "line_profiler-4.1.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:713d43be1382f47c2f04d5d25ba3c65978292249849f85746a8476d6a8863717"},
{file = "line_profiler-4.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:2ed7027f7d1b3ae9a379a2f407f512b84ccf82d6a3a7b53a90bb17ada61928a9"}, {file = "line_profiler-4.1.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6a3dd7ba3a17da254338313ec1d4ce4bdd723812e5cb58f4d05b78c1c5dbe4"},
{file = "line_profiler-4.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e8537be16b46133ab86d6e805ca83b012b17ef36a7445dd5c89c45ba70b97aad"}, {file = "line_profiler-4.1.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:481bbace88b2e15fb63a16e578a48faa28eba7399afe7da6ce1bde569780c346"},
{file = "line_profiler-4.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:934870b5e451c938f149c5475cc0286133d8718ba99ff4ec04fb1a87f7bfb985"}, {file = "line_profiler-4.1.3-cp36-cp36m-win_amd64.whl", hash = "sha256:654b16f9e82b0ce7f7657ef859bf2324275e9cd70c8169414922c9cb37d5589f"},
{file = "line_profiler-4.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbda8e0bb98b1790ba8819d0a72ee3e11e669c79fc703eaf0e5ed747cac2d441"}, {file = "line_profiler-4.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:39332137af7a562c44524cef7c37de9860428ce2cde8b9c51047ccad9fd5eca4"},
{file = "line_profiler-4.1.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cfd263c79927f74f174e32b83e4692e26ada2fefcdfef0c1dae5cfabb37a37"}, {file = "line_profiler-4.1.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dad96626acd5804c818c374d34ce1debea07b1e100b160499f4dfbcf5fc1cbe6"},
{file = "line_profiler-4.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:390f5e5dc047a62ffb7dbd236b4d44c6175d4f66aabe654f4b35df9b9aa79d02"}, {file = "line_profiler-4.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7125846d636959907e307c1f0bbf6f05fe5b7ca195b929f7b676fd20cf0763f2"},
{file = "line_profiler-4.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dce014572ee599b2d571cf45fbd0c7d5f1a1e822dabe82581e18dd0229b16799"}, {file = "line_profiler-4.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a89de2a09363dd1a62a0a49e82a7157854b6e92b1893627b14e952412357db60"},
{file = "line_profiler-4.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4fe92a239d8097a3a0cacb280e0a2455be6633da3c844b784ba011043d090b36"}, {file = "line_profiler-4.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9e11f5831a251d3a3551372b523b3bc0da1e912ab2ade2c4d9d8e0b225eed6ab"},
{file = "line_profiler-4.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3df9b30cdd8b3652e658acb38a9533bac47f2b8f5c320c5a03dbdd378ac11b35"}, {file = "line_profiler-4.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:66d856975284dc62ac6f5a97757e160c1eb9898078014385cf74b829d8d806b7"},
{file = "line_profiler-4.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5643cb19c89f6749039452913803a8cfb554c07676f6c00bc96e0632a054abb6"}, {file = "line_profiler-4.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3fb0f43900d36d7ccd8b30b8506498440d5ec610f2f1d40de3de11c3e304fb90"},
{file = "line_profiler-4.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:163d26586511b68551735052a1bcca173c9d8366573ab4a91c470c7f7bd89967"}, {file = "line_profiler-4.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7394227bfb5bf15002d3695e674916fe82c38957cd2f56fccd43b71dc3447d1e"},
{file = "line_profiler-4.1.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8fa3128e93e49ad8b5216e40dc9d2bc2e354e896c1512feead3d6db1668ce649"}, {file = "line_profiler-4.1.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8e19a0ca3198b173a5b7caa304be3b39d122f89b0cfc2a134c5cbb4105ee2fd6"},
{file = "line_profiler-4.1.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a1eb88cec273300377b364eee9ceffce2e639906bf210e7d7233c88dc87e62f"}, {file = "line_profiler-4.1.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ad57e3c80fb0aee0c86a25d738e3556063eb3d57d0a43217de13f134417915d"},
{file = "line_profiler-4.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7f213eeb846c9bc950fd210dfcd0fa93b1d2991f218b8788c0759f06bd00557"}, {file = "line_profiler-4.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cca919a8199236326f14f3719e992f30dd43a272b0e8fcb98e436a66e4a96fc"},
{file = "line_profiler-4.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ec6f137dbbdc0af6b88a1053c1430681c07a3b2d1719dc1f59be70d464851a23"}, {file = "line_profiler-4.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d6753834e1ea03ea19015d0553f0ce0d61bbf2269b85fc0f42833d616369488b"},
{file = "line_profiler-4.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3af457b2dfad6e2019f7e5bbe9eabac9b2c34824fb2ea574aee7b17998c48c98"}, {file = "line_profiler-4.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a32559afd550852f2054a441d33afe16e8b68b167ffb15373ec2b521c6fdc51f"},
{file = "line_profiler-4.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:9dd72adc753019788ff0498dd686068c4d8e65d38c0eca1b4b58b5719c14fa7d"}, {file = "line_profiler-4.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:e526f9dfad5e8e21cd5345d5213757cfc26af33f072042f3ccff36b10c46a23c"},
{file = "line_profiler-4.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:62776d67dfc6c358de5c19d606eccbd95e6feb75928064850be0232e9276f751"}, {file = "line_profiler-4.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5aec873bea3a1357c1a21f788b44d29e288df2a579b4433c8a85fc2b0a8c229d"},
{file = "line_profiler-4.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:060d71ba11ff5476d7c10774a34955566bab545ab5ff39231306b4d84081725d"}, {file = "line_profiler-4.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6059a8960487fc1e7b333178d39c53d3de5fd3c7da04477019e70d13c4c8520c"},
{file = "line_profiler-4.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ad13e1d5a174336508bbf275202822c8898cd1f014881059103b748310d5bc84"}, {file = "line_profiler-4.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2ac815ba3cdc8603de6b0ea57a725f4aea1e0a2b7d8c99fabb43f6f2b1670dc0"},
{file = "line_profiler-4.1.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77824dfc1f58dc7fe62fb053aa54586979ef60fea221dcdbba2022608c1314f"}, {file = "line_profiler-4.1.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ebd58a953fa86384150b79638331133ef0c22d8d68f046e00fe97e62053edae"},
{file = "line_profiler-4.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8ffc44a030789f7bc6594de581b39e8da0591fc6c598dd4243cf140b200528"}, {file = "line_profiler-4.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c91e4cb038496e771220daccb512dab5311619392fec59ea916e9316630e9825"},
{file = "line_profiler-4.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4729820d8da3ed92f14e30dbd28a851eeefe2ba70b8b897f2d9c886ade8007c1"}, {file = "line_profiler-4.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b4e4a49a42d4d9e1dce122dd0a5a427f9a337c22cf8a82712f006cae038870bf"},
{file = "line_profiler-4.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0bce5c04d0daf6dd19348540012b0a6d69206ae40db096de222e6d5f824922e8"}, {file = "line_profiler-4.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:209d41401238eb0da340f92dfaf60dd84500be475b2b6738cf0ef28579b4df9a"},
{file = "line_profiler-4.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:a65b70d6ecef4f2e61cf504a5c77085718f1dae9508b21c9058ad483ae7e16ee"}, {file = "line_profiler-4.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:68684974e81344344174caf723bb4ab6659bc186d05c8f7e2453002e6bf74cff"},
{file = "line_profiler-4.1.2.tar.gz", hash = "sha256:aa56578b0ff5a756fe180b3fda7bd67c27bbd478b3d0124612d8cf00e4a21df2"}, {file = "line_profiler-4.1.3.tar.gz", hash = "sha256:e5f1123c3672c3218ba063c23bd64a51159e44649fed6780b993c781fb5ed318"},
] ]
[package.extras] [package.extras]
all = ["Cython (>=3.0.3)", "IPython (>=7.14.0)", "IPython (>=7.18.0)", "IPython (>=8.12.2)", "IPython (>=8.14.0)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.8.1)", "cmake (>=3.21.2)", "coverage[toml] (>=5.3)", "ninja (>=1.10.2)", "pytest (>=4.6.0)", "pytest (>=4.6.0)", "pytest (>=4.6.0,<=4.6.11)", "pytest (>=4.6.0,<=4.6.11)", "pytest (>=4.6.0,<=6.1.2)", "pytest (>=6.2.5)", "pytest-cov (>=2.8.1)", "pytest-cov (>=2.8.1)", "pytest-cov (>=2.9.0)", "pytest-cov (>=3.0.0)", "rich (>=12.3.0)", "scikit-build (>=0.11.1)", "setuptools (>=41.0.1)", "setuptools (>=68.2.2)", "ubelt (>=1.3.4)", "xdoctest (>=1.1.2)"] all = ["Cython (>=3.0.3)", "IPython (>=7.14.0)", "IPython (>=7.18.0)", "IPython (>=8.12.2)", "IPython (>=8.14.0)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.11.2)", "cibuildwheel (>=2.8.1)", "cmake (>=3.21.2)", "coverage[toml] (>=6.1.1)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=7.3.0)", "ninja (>=1.10.2)", "pytest (>=6.2.5)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest-cov (>=3.0.0)", "rich (>=12.3.0)", "scikit-build (>=0.11.1)", "setuptools (>=41.0.1)", "setuptools (>=68.2.2)", "ubelt (>=1.3.4)", "xdoctest (>=1.1.3)"]
all-strict = ["Cython (==3.0.3)", "IPython (==7.14.0)", "IPython (==7.18.0)", "IPython (==8.12.2)", "IPython (==8.14.0)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.8.1)", "cmake (==3.21.2)", "coverage[toml] (==5.3)", "ninja (==1.10.2)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==2.8.1)", "pytest-cov (==2.8.1)", "pytest-cov (==2.9.0)", "pytest-cov (==3.0.0)", "rich (==12.3.0)", "scikit-build (==0.11.1)", "setuptools (==41.0.1)", "setuptools (==68.2.2)", "ubelt (==1.3.4)", "xdoctest (==1.1.2)"] all-strict = ["Cython (==3.0.3)", "IPython (==7.14.0)", "IPython (==7.18.0)", "IPython (==8.12.2)", "IPython (==8.14.0)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.11.2)", "cibuildwheel (==2.8.1)", "cmake (==3.21.2)", "coverage[toml] (==6.1.1)", "coverage[toml] (==6.5.0)", "coverage[toml] (==6.5.0)", "coverage[toml] (==6.5.0)", "coverage[toml] (==6.5.0)", "coverage[toml] (==7.3.0)", "ninja (==1.10.2)", "pytest (==6.2.5)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest-cov (==3.0.0)", "rich (==12.3.0)", "scikit-build (==0.11.1)", "setuptools (==41.0.1)", "setuptools (==68.2.2)", "ubelt (==1.3.4)", "xdoctest (==1.1.3)"]
ipython = ["IPython (>=7.14.0)", "IPython (>=7.18.0)", "IPython (>=8.12.2)", "IPython (>=8.14.0)"] ipython = ["IPython (>=7.14.0)", "IPython (>=7.18.0)", "IPython (>=8.12.2)", "IPython (>=8.14.0)"]
ipython-strict = ["IPython (==7.14.0)", "IPython (==7.18.0)", "IPython (==8.12.2)", "IPython (==8.14.0)"] ipython-strict = ["IPython (==7.14.0)", "IPython (==7.18.0)", "IPython (==8.12.2)", "IPython (==8.14.0)"]
optional = ["IPython (>=7.14.0)", "IPython (>=7.18.0)", "IPython (>=8.12.2)", "IPython (>=8.14.0)", "rich (>=12.3.0)"] optional = ["IPython (>=7.14.0)", "IPython (>=7.18.0)", "IPython (>=8.12.2)", "IPython (>=8.14.0)", "rich (>=12.3.0)"]
optional-strict = ["IPython (==7.14.0)", "IPython (==7.18.0)", "IPython (==8.12.2)", "IPython (==8.14.0)", "rich (==12.3.0)"] optional-strict = ["IPython (==7.14.0)", "IPython (==7.18.0)", "IPython (==8.12.2)", "IPython (==8.14.0)", "rich (==12.3.0)"]
tests = ["coverage[toml] (>=5.3)", "pytest (>=4.6.0)", "pytest (>=4.6.0)", "pytest (>=4.6.0,<=4.6.11)", "pytest (>=4.6.0,<=4.6.11)", "pytest (>=4.6.0,<=6.1.2)", "pytest (>=6.2.5)", "pytest-cov (>=2.8.1)", "pytest-cov (>=2.8.1)", "pytest-cov (>=2.9.0)", "pytest-cov (>=3.0.0)", "ubelt (>=1.3.4)", "xdoctest (>=1.1.2)"] tests = ["coverage[toml] (>=6.1.1)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=6.5.0)", "coverage[toml] (>=7.3.0)", "pytest (>=6.2.5)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest (>=7.4.4)", "pytest-cov (>=3.0.0)", "ubelt (>=1.3.4)", "xdoctest (>=1.1.3)"]
tests-strict = ["coverage[toml] (==5.3)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==2.8.1)", "pytest-cov (==2.8.1)", "pytest-cov (==2.9.0)", "pytest-cov (==3.0.0)", "ubelt (==1.3.4)", "xdoctest (==1.1.2)"] tests-strict = ["coverage[toml] (==6.1.1)", "coverage[toml] (==6.5.0)", "coverage[toml] (==6.5.0)", "coverage[toml] (==6.5.0)", "coverage[toml] (==6.5.0)", "coverage[toml] (==7.3.0)", "pytest (==6.2.5)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest (==7.4.4)", "pytest-cov (==3.0.0)", "ubelt (==1.3.4)", "xdoctest (==1.1.3)"]
[[package]] [[package]]
name = "mako" name = "mako"
@ -1464,13 +1426,13 @@ files = [
[[package]] [[package]]
name = "pyqtgraph" name = "pyqtgraph"
version = "0.13.6" version = "0.13.7"
description = "Scientific Graphics and GUI Library for Python" description = "Scientific Graphics and GUI Library for Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "pyqtgraph-0.13.6-py3-none-any.whl", hash = "sha256:35740eb7e5908e490c6e06ef6ef15738dfba27ee35b0b0417638d515da6f9226"}, {file = "pyqtgraph-0.13.7-py3-none-any.whl", hash = "sha256:7754edbefb6c367fa0dfb176e2d0610da3ada20aa7a5318516c74af5fb72bf7a"},
{file = "pyqtgraph-0.13.6.tar.gz", hash = "sha256:2397a2197e7ac66920329bf28fb346b038d85a351f8988ccc806ffb79d11573d"}, {file = "pyqtgraph-0.13.7.tar.gz", hash = "sha256:64f84f1935c6996d0e09b1ee66fe478a7771e3ca6f3aaa05f00f6e068321d9e3"},
] ]
[package.dependencies] [package.dependencies]
@ -1544,19 +1506,6 @@ pytest = "*"
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"] doc = ["sphinx", "sphinx-rtd-theme"]
[[package]]
name = "python-levenshtein"
version = "0.12.2"
description = "Python extension for computing string edit distances and similarities."
optional = false
python-versions = "*"
files = [
{file = "python-Levenshtein-0.12.2.tar.gz", hash = "sha256:dc2395fbd148a1ab31090dd113c366695934b9e85fe5a4b2a032745efd0346f6"},
]
[package.dependencies]
setuptools = "*"
[[package]] [[package]]
name = "python-slugify" name = "python-slugify"
version = "8.0.4" version = "8.0.4"
@ -1624,22 +1573,6 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"] jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "setuptools"
version = "69.5.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -1683,17 +1616,6 @@ files = [
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
] ]
[[package]]
name = "soupsieve"
version = "2.5"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
python-versions = ">=3.8"
files = [
{file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
{file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
]
[[package]] [[package]]
name = "sphinx" name = "sphinx"
version = "7.3.7" version = "7.3.7"
@ -1728,23 +1650,6 @@ docs = ["sphinxcontrib-websupport"]
lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"]
test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"]
[[package]]
name = "sphinx-basic-ng"
version = "1.0.0b2"
description = "A modern skeleton for Sphinx themes."
optional = false
python-versions = ">=3.7"
files = [
{file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"},
{file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"},
]
[package.dependencies]
sphinx = ">=4.0"
[package.extras]
docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"]
[[package]] [[package]]
name = "sphinxcontrib-applehelp" name = "sphinxcontrib-applehelp"
version = "1.0.8" version = "1.0.8"
@ -1981,20 +1886,6 @@ files = [
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
] ]
[[package]]
name = "thefuzz"
version = "0.19.0"
description = "Fuzzy string matching in python"
optional = false
python-versions = "*"
files = [
{file = "thefuzz-0.19.0-py2.py3-none-any.whl", hash = "sha256:4fcdde8e40f5ca5e8106bc7665181f9598a9c8b18b0a4d38c41a095ba6788972"},
{file = "thefuzz-0.19.0.tar.gz", hash = "sha256:6f7126db2f2c8a54212b05e3a740e45f4291c497d75d20751728f635bb74aa3d"},
]
[package.extras]
speedup = ["python-levenshtein (>=0.12)"]
[[package]] [[package]]
name = "tinytag" name = "tinytag"
version = "1.10.1" version = "1.10.1"
@ -2145,4 +2036,4 @@ test = ["websockets"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "1788eb932877661b33482412cbe7bb623a8bd8364db55bf2a1ad48d0e37cc737" content-hash = "b33fb0a465d9bea7ec2bf14800405452d2af35e377d4c02022e33d318d8f190c"

View File

@ -16,8 +16,6 @@ psutil = "^5.9.8"
pydub = "^0.25.1" pydub = "^0.25.1"
types-psutil = "^5.9.5.20240423" types-psutil = "^5.9.5.20240423"
python-slugify = "^8.0.4" python-slugify = "^8.0.4"
thefuzz = "^0.19.0"
python-Levenshtein = "^0.12.2"
pyfzf = "^0.3.1" pyfzf = "^0.3.1"
pydymenu = "^0.5.2" pydymenu = "^0.5.2"
stackprinter = "^0.2.10" stackprinter = "^0.2.10"
@ -33,13 +31,12 @@ obs-websocket-py = "^1.0"
ipdb = "^0.13.9" ipdb = "^0.13.9"
pytest-qt = "^4.4.0" pytest-qt = "^4.4.0"
pydub-stubs = "^0.25.1" pydub-stubs = "^0.25.1"
line-profiler = "^4.1.2" line-profiler = "^4.1.3"
flakehell = "^0.9.0" flakehell = "^0.9.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pudb = "*" pudb = "*"
sphinx = "^7.0.1" sphinx = "^7.0.1"
furo = "^2023.5.20"
flakehell = "^0.9.0" flakehell = "^0.9.0"
mypy = "^1.7.0" mypy = "^1.7.0"
pdbp = "^1.5.0" pdbp = "^1.5.0"

View File

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

View File

@ -1,17 +1,14 @@
# Standard library imports # Standard library imports
import os import os
import unittest import unittest
from typing import Optional
# PyQt imports # PyQt imports
from PyQt6.QtCore import Qt, QModelIndex from PyQt6.QtCore import Qt, QModelIndex
# Third party imports # Third party imports
from sqlalchemy.orm.session import Session
# App imports # App imports
from app.log import log from app.helpers import get_all_track_metadata
from app.helpers import get_file_metadata
# Set up test database before importing db # Set up test database before importing db
# Mark subsequent lines to ignore E402, imports not at top of file # Mark subsequent lines to ignore E402, imports not at top of file
@ -23,7 +20,6 @@ from app import playlistmodel # noqa: E402
from app.models import ( # noqa: E402 from app.models import ( # noqa: E402
db, db,
Playlists, Playlists,
Settings,
Tracks, Tracks,
) )
@ -51,8 +47,7 @@ class TestMMMiscTracks(unittest.TestCase):
for row in range(len(self.test_tracks)): for row in range(len(self.test_tracks)):
track_path = self.test_tracks[row % len(self.test_tracks)] track_path = self.test_tracks[row % len(self.test_tracks)]
metadata = get_file_metadata(track_path) track = Tracks(session, **get_all_track_metadata(track_path))
track = Tracks(session, **metadata)
self.model.insert_row( self.model.insert_row(
proposed_row_number=row, track_id=track.id, note=f"{row=}" proposed_row_number=row, track_id=track.id, note=f"{row=}"
) )
@ -113,7 +108,7 @@ class TestMMMiscNoPlaylist(unittest.TestCase):
_ = str(model) _ = str(model)
track_path = self.test_tracks[0] track_path = self.test_tracks[0]
metadata = get_file_metadata(track_path) metadata = get_all_track_metadata(track_path)
track = Tracks(session, **metadata) track = Tracks(session, **metadata)
model.insert_row(proposed_row_number=0, track_id=track.id) model.insert_row(proposed_row_number=0, track_id=track.id)

View File

@ -3,14 +3,12 @@ import os
import unittest import unittest
# PyQt imports # PyQt imports
from PyQt6.QtCore import Qt
# Third party imports # Third party imports
import pytest import pytest
from pytestqt.plugin import QtBot # type: ignore from pytestqt.plugin import QtBot # type: ignore
# App imports # App imports
from app import helpers
# Set up test database before importing db # Set up test database before importing db
# Mark subsequent lines to ignore E402, imports not at top of file # Mark subsequent lines to ignore E402, imports not at top of file