Compare commits
No commits in common. "b0278b92b046b6a912212d711b8af727b27d6c7e" and "2bf1e442be579c7e0983ab4374e759859c7bb736" have entirely different histories.
b0278b92b0
...
2bf1e442be
@ -5,7 +5,6 @@ from typing import List, Optional
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
AUDACITY_TIMEOUT_TENTHS = 100
|
|
||||||
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
||||||
BITRATE_LOW_THRESHOLD = 192
|
BITRATE_LOW_THRESHOLD = 192
|
||||||
BITRATE_OK_THRESHOLD = 300
|
BITRATE_OK_THRESHOLD = 300
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from helpers import (
|
|||||||
get_relative_date,
|
get_relative_date,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
)
|
)
|
||||||
from log import log
|
|
||||||
from models import Settings, Tracks
|
from models import Settings, Tracks
|
||||||
from playlistmodel import PlaylistModel
|
from playlistmodel import PlaylistModel
|
||||||
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
|
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
|
||||||
@ -70,27 +69,22 @@ class TrackSelectDialog(QDialog):
|
|||||||
track = item.data(Qt.ItemDataRole.UserRole)
|
track = item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
|
||||||
note = self.ui.txtNote.text()
|
note = self.ui.txtNote.text()
|
||||||
|
|
||||||
if not (track or note):
|
|
||||||
return
|
|
||||||
|
|
||||||
track_id = None
|
track_id = None
|
||||||
if track:
|
if track:
|
||||||
track_id = track.id
|
track_id = track.id
|
||||||
|
|
||||||
if note and not track_id:
|
if not track_id:
|
||||||
self.source_model.insert_row(self.new_row_number, track_id, note)
|
if note:
|
||||||
self.ui.txtNote.clear()
|
self.source_model.insert_row(self.new_row_number, track_id, note)
|
||||||
self.new_row_number += 1
|
self.ui.txtNote.clear()
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
# No note, no track
|
||||||
|
return
|
||||||
|
|
||||||
self.ui.txtNote.clear()
|
self.ui.txtNote.clear()
|
||||||
self.select_searchtext()
|
self.select_searchtext()
|
||||||
|
|
||||||
if track_id is None:
|
|
||||||
log.error("track_id is None and should not be")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check whether track is already in playlist
|
# Check whether track is already in playlist
|
||||||
move_existing = False
|
move_existing = False
|
||||||
existing_prd = self.source_model.is_track_in_playlist(track_id)
|
existing_prd = self.source_model.is_track_in_playlist(track_id)
|
||||||
@ -116,8 +110,6 @@ class TrackSelectDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
self.source_model.insert_row(self.new_row_number, track_id, note)
|
self.source_model.insert_row(self.new_row_number, track_id, note)
|
||||||
|
|
||||||
self.new_row_number += 1
|
|
||||||
|
|
||||||
def add_selected_and_close(self) -> None:
|
def add_selected_and_close(self) -> None:
|
||||||
"""Handle Add and Close button"""
|
"""Handle Add and Close button"""
|
||||||
|
|
||||||
|
|||||||
@ -22,11 +22,75 @@ from log import log
|
|||||||
start_time_re = re.compile(r"@\d\d:\d\d")
|
start_time_re = re.compile(r"@\d\d:\d\d")
|
||||||
|
|
||||||
|
|
||||||
|
class AudacityManager:
|
||||||
|
"""
|
||||||
|
Manage comms with Audacity
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
Open passed file in Audacity
|
||||||
|
|
||||||
|
Return True if apparently opened successfully, else False
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.to_pipe: str = "/tmp/audacity_script_pipe.to." + str(os.getuid())
|
||||||
|
self.from_pipe: str = "/tmp/audacity_script_pipe.from." + str(os.getuid())
|
||||||
|
self.eol: str = "\n"
|
||||||
|
self.path = ""
|
||||||
|
|
||||||
|
def open_file(self, path: str) -> str:
|
||||||
|
"""Open passed file in Audacity"""
|
||||||
|
|
||||||
|
self.path = path
|
||||||
|
return self._do_command(f'Import2: Filename="{self.path}"')
|
||||||
|
|
||||||
|
def export_file(self) -> str:
|
||||||
|
"""Export current file"""
|
||||||
|
|
||||||
|
if not self.path:
|
||||||
|
return "Error: no path selected"
|
||||||
|
|
||||||
|
sa_response = self._do_command("SelectAll:")
|
||||||
|
if sa_response == "\nBatchCommand finished: OK\n":
|
||||||
|
exp_response = self._do_command(
|
||||||
|
f'Export2: Filename="{self.path}" NumChannels=2'
|
||||||
|
)
|
||||||
|
return exp_response
|
||||||
|
else:
|
||||||
|
return "SelectAll response: " + sa_response
|
||||||
|
|
||||||
|
def _send_command(self, command: str) -> None:
|
||||||
|
"""Send a single command."""
|
||||||
|
self.to_audacity.write(command + self.eol)
|
||||||
|
self.to_audacity.flush()
|
||||||
|
|
||||||
|
def _get_response(self) -> str:
|
||||||
|
"""Return the command response."""
|
||||||
|
|
||||||
|
result: str = ""
|
||||||
|
line: str = ""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result += line
|
||||||
|
line = self.from_audacity.readline()
|
||||||
|
if line == "\n" and len(result) > 0:
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _do_command(self, command: str) -> str:
|
||||||
|
"""Send one command, and return the response."""
|
||||||
|
|
||||||
|
with open(self.to_pipe, "w") as self.to_audacity, open(
|
||||||
|
self.from_pipe, "rt"
|
||||||
|
) as self.from_audacity:
|
||||||
|
self._send_command(command)
|
||||||
|
response = self._get_response()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def ask_yes_no(
|
def ask_yes_no(
|
||||||
title: str,
|
title: str, question: str, default_yes: bool = False, parent: Optional[QMainWindow] = None
|
||||||
question: str,
|
|
||||||
default_yes: bool = False,
|
|
||||||
parent: Optional[QMainWindow] = None,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Ask question; return True for yes, False for no"""
|
"""Ask question; return True for yes, False for no"""
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ stderr = colorlog.StreamHandler()
|
|||||||
stderr.setLevel(Config.LOG_LEVEL_STDERR)
|
stderr.setLevel(Config.LOG_LEVEL_STDERR)
|
||||||
stderr.addFilter(local_filter)
|
stderr.addFilter(local_filter)
|
||||||
stderr_fmt = colorlog.ColoredFormatter(
|
stderr_fmt = colorlog.ColoredFormatter(
|
||||||
"%(log_color)s[%(asctime)s] %(filename)s:%(lineno)s %(message)s", datefmt="%H:%M:%S"
|
"%(log_color)s[%(asctime)s] %(leveltag)s: %(message)s", datefmt="%H:%M:%S"
|
||||||
)
|
)
|
||||||
stderr.setFormatter(stderr_fmt)
|
stderr.setFormatter(stderr_fmt)
|
||||||
log.addHandler(stderr)
|
log.addHandler(stderr)
|
||||||
|
|||||||
@ -643,8 +643,6 @@ class PlaylistRows(Base):
|
|||||||
number from starting_row to end of playlist
|
number from starting_row to end of playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.debug(f"(move_rows_down({playlist_id=}, {starting_row=}, {move_by=}")
|
|
||||||
|
|
||||||
session.execute(
|
session.execute(
|
||||||
update(PlaylistRows)
|
update(PlaylistRows)
|
||||||
.where(
|
.where(
|
||||||
@ -786,7 +784,7 @@ class Tracks(Base):
|
|||||||
except IntegrityError as error:
|
except IntegrityError as error:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
log.error(f"Error ({error=}) importing track ({path=})")
|
log.error(f"Error ({error=}) importing track ({path=})")
|
||||||
raise ValueError(error)
|
raise ValueError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls, session) -> List["Tracks"]:
|
def get_all(cls, session) -> List["Tracks"]:
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import pipeclient
|
|
||||||
from pygame import mixer
|
from pygame import mixer
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
@ -149,10 +148,7 @@ class ImportTrack(QObject):
|
|||||||
import_finished = pyqtSignal()
|
import_finished = pyqtSignal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, filenames: List[str], source_model: PlaylistModel, row_number: Optional[int]
|
||||||
filenames: List[str],
|
|
||||||
source_model: PlaylistModel,
|
|
||||||
row_number: Optional[int],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.filenames = filenames
|
self.filenames = filenames
|
||||||
@ -174,9 +170,7 @@ class ImportTrack(QObject):
|
|||||||
try:
|
try:
|
||||||
track = Tracks(session, **metadata)
|
track = Tracks(session, **metadata)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.signals.show_warning_signal.emit(
|
self.signals.show_warning_signal.emit("Error importing track", e)
|
||||||
"Error importing track", str(e)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
helpers.normalise_track(track.path)
|
helpers.normalise_track(track.path)
|
||||||
# We're importing potentially multiple tracks in a loop.
|
# We're importing potentially multiple tracks in a loop.
|
||||||
@ -226,16 +220,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.move_source_rows: Optional[List[int]] = None
|
self.move_source_rows: Optional[List[int]] = None
|
||||||
self.move_source_model: Optional[PlaylistProxyModel] = None
|
self.move_source_model: Optional[PlaylistProxyModel] = None
|
||||||
self.audacity_file_path: Optional[str] = None
|
self.audacity_file_path: Optional[str] = None
|
||||||
# Initialise Audacity access
|
|
||||||
self.audacity_client = pipeclient.PipeClient()
|
|
||||||
log.info(f"{hex(id(self.audacity_client))=}")
|
|
||||||
|
|
||||||
if Config.CARTS_HIDE:
|
if Config.CARTS_HIDE:
|
||||||
self.cartsWidget.hide()
|
self.cartsWidget.hide()
|
||||||
self.frame_6.hide()
|
self.frame_6.hide()
|
||||||
else:
|
else:
|
||||||
self.carts_init()
|
self.carts_init()
|
||||||
self.disable_selection_timing = False
|
|
||||||
self.enable_play_next_controls()
|
self.enable_play_next_controls()
|
||||||
self.clock_counter = 0
|
self.clock_counter = 0
|
||||||
self.timer10.start(10)
|
self.timer10.start(10)
|
||||||
@ -996,11 +986,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
unplayed_rows = self.active_proxy_model().get_unplayed_rows()
|
unplayed_rows = self.active_proxy_model().get_unplayed_rows()
|
||||||
if not unplayed_rows:
|
if not unplayed_rows:
|
||||||
return
|
return
|
||||||
# We can get a race condition as selected rows change while
|
|
||||||
# moving so disable selected rows timing for move
|
|
||||||
self.disable_selection_timing = True
|
|
||||||
self.move_playlist_rows(unplayed_rows)
|
self.move_playlist_rows(unplayed_rows)
|
||||||
self.disable_selection_timing = False
|
|
||||||
|
|
||||||
def new_from_template(self) -> None:
|
def new_from_template(self) -> None:
|
||||||
"""Create new playlist from template"""
|
"""Create new playlist from template"""
|
||||||
@ -1050,7 +1036,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if self.move_source_rows is None or self.move_source_model is None:
|
if self.move_source_rows is None or self.move_source_model is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
to_playlist_model: PlaylistModel = self.active_tab().source_model
|
to_playlist_model = self.active_tab().source_model
|
||||||
selected_rows = self.active_tab().get_selected_rows()
|
selected_rows = self.active_tab().get_selected_rows()
|
||||||
if selected_rows:
|
if selected_rows:
|
||||||
destination_row = selected_rows[0]
|
destination_row = selected_rows[0]
|
||||||
@ -1064,9 +1050,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
||||||
else:
|
else:
|
||||||
self.move_source_model.move_rows_between_playlists(
|
self.move_source_model.move_rows_between_playlists(
|
||||||
self.move_source_rows, destination_row, to_playlist_model.playlist_id
|
self.move_source_rows, destination_row, to_playlist_model
|
||||||
)
|
)
|
||||||
self.active_tab().clear_selection()
|
self.move_source_rows = self.move_source_model = None
|
||||||
|
|
||||||
def play_next(self, position: Optional[float] = None) -> None:
|
def play_next(self, position: Optional[float] = None) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1122,18 +1108,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if not track_sequence.now.path:
|
if not track_sequence.now.path:
|
||||||
log.error("No path for next track")
|
log.error("No path for next track")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Notify model
|
|
||||||
self.active_proxy_model().current_track_started()
|
|
||||||
|
|
||||||
# Note that track is now playing
|
|
||||||
self.playing = True
|
|
||||||
|
|
||||||
# Disable play next controls
|
|
||||||
self.disable_play_next_controls()
|
|
||||||
|
|
||||||
# Update headers
|
|
||||||
self.update_headers()
|
|
||||||
track_sequence.now.start()
|
track_sequence.now.start()
|
||||||
self.music.play(track_sequence.now.path, position)
|
self.music.play(track_sequence.now.path, position)
|
||||||
|
|
||||||
@ -1147,25 +1121,21 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
volume = self.music.player.audio_get_volume()
|
volume = self.music.player.audio_get_volume()
|
||||||
if volume < Config.VOLUME_VLC_DEFAULT:
|
if volume < Config.VOLUME_VLC_DEFAULT:
|
||||||
self.music.set_volume()
|
self.music.set_volume()
|
||||||
log.debug(f"Reset from {volume=}")
|
log.error(f"Reset from {volume=}")
|
||||||
break
|
break
|
||||||
sleep(0.1)
|
sleep(0.1)
|
||||||
|
|
||||||
# Try making playing the last thing we do here to see whether
|
# Notify model
|
||||||
# the occasional short pause at the start of the track can be
|
self.active_proxy_model().current_track_started()
|
||||||
# eliminated.
|
|
||||||
|
|
||||||
# # Notify model
|
# Note that track is now playing
|
||||||
# self.active_proxy_model().current_track_started()
|
self.playing = True
|
||||||
|
|
||||||
# # Note that track is now playing
|
# Disable play next controls
|
||||||
# self.playing = True
|
self.disable_play_next_controls()
|
||||||
|
|
||||||
# # Disable play next controls
|
# Update headers
|
||||||
# self.disable_play_next_controls()
|
self.update_headers()
|
||||||
|
|
||||||
# # Update headers
|
|
||||||
# self.update_headers()
|
|
||||||
|
|
||||||
def preview(self) -> None:
|
def preview(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1382,13 +1352,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.tabPlaylist.setCurrentIndex(idx)
|
self.tabPlaylist.setCurrentIndex(idx)
|
||||||
break
|
break
|
||||||
|
|
||||||
display_row = (
|
display_row = self.active_proxy_model().mapFromSource(
|
||||||
self.active_proxy_model()
|
self.active_proxy_model().source_model.index(plt.plr_rownum, 0)
|
||||||
.mapFromSource(
|
).row()
|
||||||
self.active_proxy_model().source_model.index(plt.plr_rownum, 0)
|
|
||||||
)
|
|
||||||
.row()
|
|
||||||
)
|
|
||||||
self.tabPlaylist.currentWidget().scroll_to_top(display_row)
|
self.tabPlaylist.currentWidget().scroll_to_top(display_row)
|
||||||
|
|
||||||
def solicit_playlist_name(
|
def solicit_playlist_name(
|
||||||
|
|||||||
@ -1,293 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
"""Automate Audacity via mod-script-pipe.
|
|
||||||
|
|
||||||
Pipe Client may be used as a command-line script to send commands to
|
|
||||||
Audacity via the mod-script-pipe interface, or loaded as a module.
|
|
||||||
Requires Python 3.
|
|
||||||
(Python 2.7 is now obsolete, so no longer supported)
|
|
||||||
|
|
||||||
======================
|
|
||||||
Command Line Interface
|
|
||||||
======================
|
|
||||||
|
|
||||||
usage: pipeclient.py [-h] [-t] [-s ] [-d]
|
|
||||||
|
|
||||||
Arguments
|
|
||||||
---------
|
|
||||||
-h,--help: optional
|
|
||||||
show short help and exit
|
|
||||||
-t, --timeout: float, optional
|
|
||||||
timeout for reply in seconds (default: 10)
|
|
||||||
-s, --show-time: bool, optional
|
|
||||||
show command execution time (default: True)
|
|
||||||
-d, --docs: optional
|
|
||||||
show this documentation and exit
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
$ python3 pipeclient.py -t 20 -s False
|
|
||||||
|
|
||||||
Launches command line interface with 20 second time-out for
|
|
||||||
returned message, and don't show the execution time.
|
|
||||||
|
|
||||||
When prompted, enter the command to send (not quoted), or 'Q' to quit.
|
|
||||||
|
|
||||||
$ Enter command or 'Q' to quit: GetInfo: Type=Tracks Format=LISP
|
|
||||||
|
|
||||||
============
|
|
||||||
Module Usage
|
|
||||||
============
|
|
||||||
|
|
||||||
Note that on a critical error (such as broken pipe), the module just exits.
|
|
||||||
If a more graceful shutdown is required, replace the sys.exit()'s with
|
|
||||||
exceptions.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
|
|
||||||
# Import the module:
|
|
||||||
>>> import pipeclient
|
|
||||||
|
|
||||||
# Create a client instance:
|
|
||||||
>>> client = pipeclient.PipeClient()
|
|
||||||
|
|
||||||
# Send a command:
|
|
||||||
>>> client.write("Command", timer=True)
|
|
||||||
|
|
||||||
# Read the last reply:
|
|
||||||
>>> print(client.read())
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
PipeClient.write : Write a command to _write_pipe.
|
|
||||||
PipeClient.read : Read Audacity's reply from pipe.
|
|
||||||
|
|
||||||
Copyright Steve Daulton 2018
|
|
||||||
Released under terms of the GNU General Public License version 2:
|
|
||||||
<http://www.gnu.org/licenses/old-licenses/gpl-2.0.html />
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import errno
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info[0] < 3:
|
|
||||||
sys.exit('PipeClient Error: Python 3.x required')
|
|
||||||
|
|
||||||
# Platform specific constants
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
WRITE_NAME: str = '\\\\.\\pipe\\ToSrvPipe'
|
|
||||||
READ_NAME: str = '\\\\.\\pipe\\FromSrvPipe'
|
|
||||||
EOL: str = '\r\n\0'
|
|
||||||
else:
|
|
||||||
# Linux or Mac
|
|
||||||
PIPE_BASE: str = '/tmp/audacity_script_pipe.'
|
|
||||||
WRITE_NAME: str = PIPE_BASE + 'to.' + str(os.getuid())
|
|
||||||
READ_NAME: str = PIPE_BASE + 'from.' + str(os.getuid())
|
|
||||||
EOL: str = '\n'
|
|
||||||
|
|
||||||
|
|
||||||
class PipeClient():
|
|
||||||
"""Write / read client access to Audacity via named pipes.
|
|
||||||
|
|
||||||
Normally there should be just one instance of this class. If
|
|
||||||
more instances are created, they all share the same state.
|
|
||||||
|
|
||||||
__init__ calls _write_thread_start() and _read_thread_start() on
|
|
||||||
first instantiation.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
None
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
reader_pipe_broken : event object
|
|
||||||
Set if pipe reader fails. Audacity may have crashed
|
|
||||||
reply_ready : event object
|
|
||||||
flag cleared when command sent and set when response received
|
|
||||||
timer : bool
|
|
||||||
When true, time the command execution (default False)
|
|
||||||
reply : string
|
|
||||||
message received when Audacity completes the command
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
write : Write a command to _write_pipe.
|
|
||||||
read : Read Audacity's reply from pipe.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
reader_pipe_broken = threading.Event()
|
|
||||||
reply_ready = threading.Event()
|
|
||||||
|
|
||||||
_shared_state: dict = {}
|
|
||||||
|
|
||||||
def __new__(cls, *p, **k):
|
|
||||||
self = object.__new__(cls, *p, **k)
|
|
||||||
self.__dict__ = cls._shared_state
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.timer: bool = False
|
|
||||||
self._start_time: float = 0
|
|
||||||
self._write_pipe = None
|
|
||||||
self.reply: str = ''
|
|
||||||
if not self._write_pipe:
|
|
||||||
self._write_thread_start()
|
|
||||||
self._read_thread_start()
|
|
||||||
|
|
||||||
def _write_thread_start(self) -> None:
|
|
||||||
"""Start _write_pipe thread"""
|
|
||||||
# Pipe is opened in a new thread so that we don't
|
|
||||||
# freeze if Audacity is not running.
|
|
||||||
write_thread = threading.Thread(target=self._write_pipe_open)
|
|
||||||
write_thread.daemon = True
|
|
||||||
write_thread.start()
|
|
||||||
# Allow a little time for connection to be made.
|
|
||||||
time.sleep(0.1)
|
|
||||||
if not self._write_pipe:
|
|
||||||
sys.exit('PipeClientError: Write pipe cannot be opened.')
|
|
||||||
|
|
||||||
def _write_pipe_open(self) -> None:
|
|
||||||
"""Open _write_pipe."""
|
|
||||||
self._write_pipe = open(WRITE_NAME, 'w', encoding='ascii')
|
|
||||||
|
|
||||||
def _read_thread_start(self) -> None:
|
|
||||||
"""Start read_pipe thread."""
|
|
||||||
read_thread = threading.Thread(target=self._reader)
|
|
||||||
read_thread.daemon = True
|
|
||||||
read_thread.start()
|
|
||||||
|
|
||||||
def write(self, command, timer=False) -> None:
|
|
||||||
"""Write a command to _write_pipe.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
command : string
|
|
||||||
The command to send to Audacity
|
|
||||||
timer : bool, optional
|
|
||||||
If true, time the execution of the command
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
write("GetInfo: Type=Labels", timer=True):
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.timer = timer
|
|
||||||
self._write_pipe.write(command + EOL)
|
|
||||||
# Check that read pipe is alive
|
|
||||||
if PipeClient.reader_pipe_broken.is_set():
|
|
||||||
sys.exit('PipeClient: Read-pipe error.')
|
|
||||||
try:
|
|
||||||
self._write_pipe.flush()
|
|
||||||
if self.timer:
|
|
||||||
self._start_time = time.time()
|
|
||||||
self.reply = ''
|
|
||||||
PipeClient.reply_ready.clear()
|
|
||||||
except IOError as err:
|
|
||||||
if err.errno == errno.EPIPE:
|
|
||||||
sys.exit('PipeClient: Write-pipe error.')
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _reader(self) -> None:
|
|
||||||
"""Read FIFO in worker thread."""
|
|
||||||
# Thread will wait at this read until it connects.
|
|
||||||
# Connection should occur as soon as _write_pipe has connected.
|
|
||||||
with open(READ_NAME, 'r', encoding='ascii') as read_pipe:
|
|
||||||
message = ''
|
|
||||||
pipe_ok = True
|
|
||||||
while pipe_ok:
|
|
||||||
line = read_pipe.readline()
|
|
||||||
# Stop timer as soon as we get first line of response.
|
|
||||||
stop_time = time.time()
|
|
||||||
while pipe_ok and line != '\n':
|
|
||||||
message += line
|
|
||||||
line = read_pipe.readline()
|
|
||||||
if line == '':
|
|
||||||
# No data in read_pipe indicates that the pipe
|
|
||||||
# is broken (Audacity may have crashed).
|
|
||||||
PipeClient.reader_pipe_broken.set()
|
|
||||||
pipe_ok = False
|
|
||||||
if self.timer:
|
|
||||||
xtime = (stop_time - self._start_time) * 1000
|
|
||||||
message += f'Execution time: {xtime:.2f}ms'
|
|
||||||
self.reply = message
|
|
||||||
PipeClient.reply_ready.set()
|
|
||||||
message = ''
|
|
||||||
|
|
||||||
def read(self) -> str:
|
|
||||||
"""Read Audacity's reply from pipe.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
string
|
|
||||||
The reply from the last command sent to Audacity, or null string
|
|
||||||
if reply not received. Null string usually indicates that Audacity
|
|
||||||
is still processing the last command.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not PipeClient.reply_ready.is_set():
|
|
||||||
return ''
|
|
||||||
return self.reply
|
|
||||||
|
|
||||||
|
|
||||||
def bool_from_string(strval) -> bool:
|
|
||||||
"""Return boolean value from string"""
|
|
||||||
if strval.lower() in ('true', 't', '1', 'yes', 'y'):
|
|
||||||
return True
|
|
||||||
if strval.lower() in ('false', 'f', '0', 'no', 'n'):
|
|
||||||
return False
|
|
||||||
raise argparse.ArgumentTypeError('Boolean value expected.')
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Interactive command-line for PipeClient"""
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('-t', '--timeout', type=float, metavar='', default=10,
|
|
||||||
help="timeout for reply in seconds (default: 10")
|
|
||||||
parser.add_argument('-s', '--show-time', metavar='True/False',
|
|
||||||
nargs='?', type=bool_from_string,
|
|
||||||
const='t', default='t', dest='show',
|
|
||||||
help='show command execution time (default: True)')
|
|
||||||
parser.add_argument('-d', '--docs', action='store_true',
|
|
||||||
help='show documentation and exit')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.docs:
|
|
||||||
print(__doc__)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
client: PipeClient = PipeClient()
|
|
||||||
while True:
|
|
||||||
reply: str = ''
|
|
||||||
message: str = input("\nEnter command or 'Q' to quit: ")
|
|
||||||
start = time.time()
|
|
||||||
if message.upper() == 'Q':
|
|
||||||
sys.exit(0)
|
|
||||||
elif message == '':
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
client.write(message, timer=args.show)
|
|
||||||
while reply == '':
|
|
||||||
time.sleep(0.1) # allow time for reply
|
|
||||||
if time.time() - start > args.timeout:
|
|
||||||
reply = 'PipeClient: Reply timed-out.'
|
|
||||||
else:
|
|
||||||
reply = client.read()
|
|
||||||
print(reply)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@ -440,10 +440,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
End model reset if this is our playlist
|
End model reset if this is our playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.debug(f"end_reset_model({playlist_id=})")
|
|
||||||
|
|
||||||
if playlist_id != self.playlist_id:
|
if playlist_id != self.playlist_id:
|
||||||
log.debug(f"end_reset_model: not us ({self.playlist_id=})")
|
|
||||||
return
|
return
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
@ -532,7 +529,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
If not given, return row number to add to end of model.
|
If not given, return row number to add to end of model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(f"_get_new_row_number({proposed_row_number=})")
|
log.info(f"get_duplicate_rows({proposed_row_number=})")
|
||||||
|
|
||||||
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
|
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
|
||||||
# We are adding to the end of the list
|
# We are adding to the end of the list
|
||||||
@ -880,16 +877,18 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.invalidate_rows(list(row_map.keys()))
|
self.invalidate_rows(list(row_map.keys()))
|
||||||
|
|
||||||
def move_rows_between_playlists(
|
def move_rows_between_playlists(
|
||||||
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
self, from_rows: List[int], to_row_number: int, to_playlist_model: PlaylistModel
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Move the playlist rows given to to_row and below of to_playlist.
|
Move the playlist rows given to to_row and below of to_playlist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
f"move_rows_between_playlists({from_rows=}, {to_row_number=}, {to_playlist_id=}"
|
f"move_rows_between_playlists({from_rows=}, {to_row_number=}, {to_playlist_model=}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
to_playlist_id = to_playlist_model.playlist_id
|
||||||
|
|
||||||
# Row removal must be wrapped in beginRemoveRows ..
|
# Row removal must be wrapped in beginRemoveRows ..
|
||||||
# endRemoveRows and the row range must be contiguous. Process
|
# endRemoveRows and the row range must be contiguous. Process
|
||||||
# the highest rows first so the lower row numbers are unchanged
|
# the highest rows first so the lower row numbers are unchanged
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import psutil
|
import psutil
|
||||||
import time
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
@ -36,6 +35,7 @@ from classes import MusicMusterSignals, track_sequence
|
|||||||
from config import Config
|
from config import Config
|
||||||
from helpers import (
|
from helpers import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
|
AudacityManager,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
show_OK,
|
show_OK,
|
||||||
show_warning,
|
show_warning,
|
||||||
@ -292,20 +292,15 @@ class PlaylistTab(QTableView):
|
|||||||
if len(selected_rows) == 0:
|
if len(selected_rows) == 0:
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
self.musicmuster.lblSumPlaytime.setText("")
|
||||||
else:
|
else:
|
||||||
if not self.musicmuster.disable_selection_timing:
|
selected_duration = self.source_model.get_rows_duration(
|
||||||
selected_duration = self.source_model.get_rows_duration(
|
self.get_selected_rows()
|
||||||
self.get_selected_rows()
|
)
|
||||||
|
if selected_duration > 0:
|
||||||
|
self.musicmuster.lblSumPlaytime.setText(
|
||||||
|
f"Selected duration: {ms_to_mmss(selected_duration)}"
|
||||||
)
|
)
|
||||||
if selected_duration > 0:
|
|
||||||
self.musicmuster.lblSumPlaytime.setText(
|
|
||||||
f"Selected duration: {ms_to_mmss(selected_duration)}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
|
||||||
else:
|
else:
|
||||||
log.info(
|
self.musicmuster.lblSumPlaytime.setText("")
|
||||||
f"playlists.py.selectionChanged: {self.musicmuster.disable_selection_timing=}"
|
|
||||||
)
|
|
||||||
|
|
||||||
super().selectionChanged(selected, deselected)
|
super().selectionChanged(selected, deselected)
|
||||||
|
|
||||||
@ -348,46 +343,6 @@ class PlaylistTab(QTableView):
|
|||||||
)
|
)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
def _audactity_command(self, cmd: str) -> bool:
|
|
||||||
"""
|
|
||||||
Send cmd to Audacity and monitor for response. Return True if successful
|
|
||||||
else False.
|
|
||||||
"""
|
|
||||||
|
|
||||||
log.info(f"_audacity({cmd=})")
|
|
||||||
|
|
||||||
# Notify user if audacity not running
|
|
||||||
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
|
||||||
log.warning("Audactity not running")
|
|
||||||
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.musicmuster.audacity_client.write(cmd, timer=True)
|
|
||||||
|
|
||||||
reply = ""
|
|
||||||
count = 0
|
|
||||||
while reply == "" and count < Config.AUDACITY_TIMEOUT_TENTHS:
|
|
||||||
time.sleep(0.1)
|
|
||||||
reply = self.musicmuster.audacity_client.read()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
log.debug(f"_audactity_command: {count=}, {reply=}")
|
|
||||||
status = False
|
|
||||||
timing = ""
|
|
||||||
msgs = reply.split("\n")
|
|
||||||
for msg in msgs:
|
|
||||||
if msg == "BatchCommand finished: OK":
|
|
||||||
status = True
|
|
||||||
elif msg.startswith("Execution time:"):
|
|
||||||
timing = msg
|
|
||||||
if not status:
|
|
||||||
log.error(f"_audactity_command {msgs=}")
|
|
||||||
return False
|
|
||||||
if timing:
|
|
||||||
log.info(f"_audactity_command {timing=}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _build_context_menu(self, item: QTableWidgetItem) -> None:
|
def _build_context_menu(self, item: QTableWidgetItem) -> None:
|
||||||
"""Used to process context (right-click) menu, which is defined here"""
|
"""Used to process context (right-click) menu, which is defined here"""
|
||||||
|
|
||||||
@ -619,29 +574,16 @@ class PlaylistTab(QTableView):
|
|||||||
Import current Audacity track to passed row
|
Import current Audacity track to passed row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = self.source_model.get_row_track_path(row_number)
|
# Notify user if audacity not running
|
||||||
if not path:
|
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
||||||
log.error(f"_import_from_audacity: can't get path for {row_number=}")
|
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
|
||||||
return
|
return
|
||||||
|
|
||||||
select_cmd = "SelectAll:"
|
audacity = self.audacity
|
||||||
status = self._audactity_command(select_cmd)
|
if not audacity:
|
||||||
if not status:
|
|
||||||
log.error(f"_import_from_audacity select {status=}")
|
|
||||||
show_warning(
|
|
||||||
self.musicmuster, "Audacity", "Error selecting track in Audacity"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
export_cmd = f'Export2: Filename="{path}" NumChannels=2'
|
|
||||||
status = self._audactity_command(export_cmd)
|
|
||||||
if not status:
|
|
||||||
log.error(f"_import_from_audacity export {status=}")
|
|
||||||
show_warning(
|
|
||||||
self.musicmuster, "Audacity", "Error exporting track from Audacity"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
audacity.export_file()
|
||||||
self.musicmuster.audacity_file_path = None
|
self.musicmuster.audacity_file_path = None
|
||||||
self._rescan(row_number)
|
self._rescan(row_number)
|
||||||
|
|
||||||
@ -675,17 +617,18 @@ class PlaylistTab(QTableView):
|
|||||||
Open track in passed row in Audacity
|
Open track in passed row in Audacity
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Notify user if audacity not running
|
||||||
|
if "audacity" not in [i.name() for i in psutil.process_iter()]:
|
||||||
|
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
|
||||||
|
return
|
||||||
|
|
||||||
path = self.source_model.get_row_track_path(row_number)
|
path = self.source_model.get_row_track_path(row_number)
|
||||||
if not path:
|
if not path:
|
||||||
log.error(f"_open_in_audacity: can't get path for {row_number=}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.musicmuster.audacity_file_path = path
|
self.musicmuster.audacity_file_path = path
|
||||||
escaped_path = path.replace('"', '\\"')
|
self.audacity = AudacityManager()
|
||||||
cmd = f'Import2: Filename="{escaped_path}"'
|
self.audacity.open_file(path)
|
||||||
status = self._audactity_command(cmd)
|
|
||||||
|
|
||||||
log.info(f"_open_in_audacity {path=}, {status=}")
|
|
||||||
|
|
||||||
def _rescan(self, row_number: int) -> None:
|
def _rescan(self, row_number: int) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user