Right-click menu mostly working

Still to implement:
 - Move to playlist
 - Remove row
This commit is contained in:
Keith Edmunds 2022-08-07 20:20:56 +01:00
parent 89781c0a94
commit 99409e8626
7 changed files with 339 additions and 303 deletions

View File

@ -60,6 +60,7 @@ class Config(object):
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
SCROLL_TOP_MARGIN = 3
TESTMODE = True
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
TOD_TIME_FORMAT = "%H:%M:%S"
TIMER_MS = 500
TRACK_TIME_FORMAT = "%H:%M:%S"

View File

@ -1,11 +1,12 @@
import os
# import psutil
#
# from config import Config
import psutil
from config import Config
from datetime import datetime
# from pydub import AudioSegment
from pydub import AudioSegment
# from PyQt5.QtWidgets import QMessageBox
# from tinytag import TinyTag
from typing import Optional
# from typing import Dict, Optional, Union
#
#
@ -15,35 +16,35 @@ from datetime import datetime
# button_reply: bool = QMessageBox.question(None, title, question)
#
# return button_reply == QMessageBox.Yes
#
#
# def fade_point(
# audio_segment: AudioSegment, fade_threshold: int = 0,
# chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
# """
# Returns the millisecond/index of the point where the volume drops below
# the maximum and doesn't get louder again.
# audio_segment - the sdlg_search_database_uiegment to find silence in
# fade_threshold - the upper bound for how quiet is silent in dFBS
# chunk_size - chunk size for interating over the segment in ms
# """
#
# assert chunk_size > 0 # to avoid infinite loop
#
# segment_length: int = audio_segment.duration_seconds * 1000 # ms
# trim_ms: int = segment_length - chunk_size
# max_vol: int = audio_segment.dBFS
# if fade_threshold == 0:
# fade_threshold = max_vol
#
# while (
# audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
# and trim_ms > 0): # noqa W503
# trim_ms -= chunk_size
#
# # if there is no trailing silence, return lenght of track (it's less
# # the chunk_size, but for chunk_size = 10ms, this may be ignored)
# return int(trim_ms)
def fade_point(
audio_segment: AudioSegment, fade_threshold: float = 0.0,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
"""
Returns the millisecond/index of the point where the volume drops below
the maximum and doesn't get louder again.
audio_segment - the sdlg_search_database_uiegment to find silence in
fade_threshold - the upper bound for how quiet is silent in dFBS
chunk_size - chunk size for interating over the segment in ms
"""
assert chunk_size > 0 # to avoid infinite loop
segment_length: int = audio_segment.duration_seconds * 1000 # ms
trim_ms = segment_length - chunk_size
max_vol = audio_segment.dBFS
if fade_threshold == 0:
fade_threshold = max_vol
while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
and trim_ms > 0): # noqa W503
trim_ms -= chunk_size
# if there is no trailing silence, return lenght of track (it's less
# the chunk_size, but for chunk_size = 10ms, this may be ignored)
return int(trim_ms)
def file_is_readable(path: str, check_colon: bool = True) -> bool:
@ -60,16 +61,19 @@ def file_is_readable(path: str, check_colon: bool = True) -> bool:
return True
return False
#
# def get_audio_segment(path: str) -> Optional[AudioSegment]:
# try:
# if path.endswith('.mp3'):
# return AudioSegment.from_mp3(path)
# elif path.endswith('.flac'):
# return AudioSegment.from_file(path, "flac")
# except AttributeError:
# return None
#
def get_audio_segment(path: str) -> Optional[AudioSegment]:
try:
if path.endswith('.mp3'):
return AudioSegment.from_mp3(path)
elif path.endswith('.flac'):
return AudioSegment.from_file(path, "flac")
except AttributeError:
return None
return None
#
# def get_tags(path: str) -> Dict[str, Union[str, int]]:
# """
@ -126,32 +130,32 @@ def get_relative_date(past_date: datetime, reference_date: datetime = None) \
else:
days_str = "days"
return f"{weeks} {weeks_str}, {days} {days_str} ago"
#
#
# def leading_silence(
# audio_segment: AudioSegment,
# silence_threshold: int = Config.DBFS_SILENCE,
# chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
# """
# Returns the millisecond/index that the leading silence ends.
# audio_segment - the segment to find silence in
# silence_threshold - the upper bound for how quiet is silent in dFBS
# chunk_size - chunk size for interating over the segment in ms
#
# https://github.com/jiaaro/pydub/blob/master/pydub/silence.py
# """
#
# trim_ms: int = 0 # ms
# assert chunk_size > 0 # to avoid infinite loop
# while (
# audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504
# silence_threshold and trim_ms < len(audio_segment)):
# trim_ms += chunk_size
#
# # if there is no end it should return the length of the segment
# return min(trim_ms, len(audio_segment))
#
#
def leading_silence(
audio_segment: AudioSegment,
silence_threshold: int = Config.DBFS_SILENCE,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
"""
Returns the millisecond/index that the leading silence ends.
audio_segment - the segment to find silence in
silence_threshold - the upper bound for how quiet is silent in dFBS
chunk_size - chunk size for interating over the segment in ms
https://github.com/jiaaro/pydub/blob/master/pydub/silence.py
"""
trim_ms: int = 0 # ms
assert chunk_size > 0 # to avoid infinite loop
while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504
silence_threshold and trim_ms < len(audio_segment)):
trim_ms += chunk_size
# if there is no end it should return the length of the segment
return min(trim_ms, len(audio_segment))
def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str:
"""Convert milliseconds to mm:ss"""
@ -177,66 +181,68 @@ def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str:
seconds = 59.0
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
#
#
# def open_in_audacity(path: str) -> Optional[bool]:
# """
# Open passed file in Audacity
#
# Return True if apparently opened successfully, else False
# """
#
# # Return if audacity not running
# if "audacity" not in [i.name() for i in psutil.process_iter()]:
# return False
#
# # Return if path not given
# if not path:
# return False
#
# to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
# from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
# eol: str = '\n'
#
# def send_command(command: str) -> None:
# """Send a single command."""
# to_audacity.write(command + eol)
# to_audacity.flush()
#
# def get_response() -> str:
# """Return the command response."""
#
# result: str = ''
# line: str = ''
#
# while True:
# result += line
# line = from_audacity.readline()
# if line == '\n' and len(result) > 0:
# break
# return result
#
# def do_command(command: str) -> str:
# """Send one command, and return the response."""
#
# send_command(command)
# response = get_response()
# return response
#
# with open(to_pipe, 'w') as to_audacity, open(
# from_pipe, 'rt') as from_audacity:
# do_command(f'Import2: Filename="{path}"')
def open_in_audacity(path: str) -> bool:
"""
Open passed file in Audacity
Return True if apparently opened successfully, else False
"""
# Return if audacity not running
if "audacity" not in [i.name() for i in psutil.process_iter()]:
return False
# Return if path not given
if not path:
return False
to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
eol: str = '\n'
def send_command(command: str) -> None:
"""Send a single command."""
to_audacity.write(command + eol)
to_audacity.flush()
def get_response() -> str:
"""Return the command response."""
result: str = ''
line: str = ''
while True:
result += line
line = from_audacity.readline()
if line == '\n' and len(result) > 0:
break
return result
def do_command(command: str) -> str:
"""Send one command, and return the response."""
send_command(command)
response = get_response()
return response
with open(to_pipe, 'w') as to_audacity, open(
from_pipe, 'rt') as from_audacity:
do_command(f'Import2: Filename="{path}"')
return True
#
#
# def show_warning(title: str, msg: str) -> None:
# """Display a warning to user"""
#
# QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
#
#
# def trailing_silence(
# audio_segment: AudioSegment, silence_threshold: int = -50,
# chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
# """Return fade point from start in milliseconds"""
#
# return fade_point(audio_segment, silence_threshold, chunk_size)
def trailing_silence(
audio_segment: AudioSegment, silence_threshold: int = -50,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int:
"""Return fade point from start in milliseconds"""
return fade_point(audio_segment, silence_threshold, chunk_size)

View File

@ -1,6 +1,6 @@
#!/usr/bin/python3
#
# import os.path
import os.path
import re
#
from dbconfig import Session
@ -36,13 +36,13 @@ from sqlalchemy.orm.exc import (
NoResultFound
)
#
# from config import Config
# from helpers import (
# fade_point,
# get_audio_segment,
# leading_silence,
# trailing_silence,
# )
from config import Config
from helpers import (
fade_point,
get_audio_segment,
leading_silence,
trailing_silence,
)
from log import log
#
Base = declarative_base()
@ -669,22 +669,22 @@ class Tracks(Base):
# except NoResultFound:
# log.error(f"get_track({track_id}): not found")
# return None
#
# def rescan(self, session: Session) -> None:
# """
# Update audio metadata for passed track.
# """
#
# audio: AudioSegment = get_audio_segment(self.path)
# self.duration = len(audio)
# self.fade_at = round(fade_point(audio) / 1000,
# Config.MILLISECOND_SIGFIGS) * 1000
# self.mtime = os.path.getmtime(self.path)
# self.silence_at = round(trailing_silence(audio) / 1000,
# Config.MILLISECOND_SIGFIGS) * 1000
# self.start_gap = leading_silence(audio)
# session.add(self)
# session.flush()
def rescan(self, session: Session) -> None:
"""
Update audio metadata for passed track.
"""
audio: AudioSegment = get_audio_segment(self.path)
self.duration = len(audio)
self.fade_at = round(fade_point(audio) / 1000,
Config.MILLISECOND_SIGFIGS) * 1000
self.mtime = os.path.getmtime(self.path)
self.silence_at = round(trailing_silence(audio) / 1000,
Config.MILLISECOND_SIGFIGS) * 1000
self.start_gap = leading_silence(audio)
session.add(self)
session.flush()
#
# @staticmethod
# def remove_by_path(session: Session, path: str) -> None:

View File

@ -37,7 +37,7 @@ from models import (
# Playdates,
Playlists,
# Settings,
# Tracks
Tracks
)
from playlists import PlaylistTab
# from sqlalchemy.orm.exc import DetachedInstanceError
@ -501,10 +501,13 @@ class Window(QMainWindow, Ui_MainWindow):
for playlist in Playlists.get_open(session):
self.create_playlist_tab(session, playlist)
playlist.mark_open(session)
#
# def move_selected(self) -> None:
# """Move selected rows to another playlist"""
#
def move_selected(self) -> None:
"""Move selected rows to another playlist"""
# ***KAE
pass
# with Session() as session:
# visible_tab = self.visible_playlist_tab()
# visible_tab_id = visible_tab.playlist_id
@ -819,56 +822,59 @@ class Window(QMainWindow, Ui_MainWindow):
#
# # Run end-of-track actions
# self.end_of_track_actions()
#
# def this_is_the_next_track(self, playlist_tab: PlaylistTab,
# track: Tracks, session) -> None:
# """
# This is notification from a playlist tab that it holds the next
# track to be played.
#
# Actions required:
# - Clear next track if on other tab
# - Reset tab colour if on other tab
# - Note next playlist tab
# - Set next playlist_tab tab colour
# - Note next track
# - Update headers
# - Populate info tabs
#
# """
#
# # Clear next track if on another tab
# if self.next_track_playlist_tab != playlist_tab:
# # We need to reset the ex-next-track playlist
# if self.next_track_playlist_tab:
# self.next_track_playlist_tab.clear_next(session)
#
# # Reset tab colour if on other tab
# if (self.next_track_playlist_tab !=
# self.current_track_playlist_tab):
# self.set_tab_colour(
# self.next_track_playlist_tab,
# QColor(Config.COLOUR_NORMAL_TAB))
#
# # Note next playlist tab
# self.next_track_playlist_tab = playlist_tab
#
# # Set next playlist_tab tab colour if it isn't the
# # currently-playing tab
# if (self.next_track_playlist_tab !=
# self.current_track_playlist_tab):
# self.set_tab_colour(
# self.next_track_playlist_tab,
# QColor(Config.COLOUR_NEXT_TAB))
#
# # Note next track
# self.next_track = TrackData(track)
#
# # Update headers
# self.update_headers()
#
# # Populate 'info' tabs
# self.open_info_tabs()
def this_is_the_next_track(self, playlist_tab: PlaylistTab,
track: Tracks, session) -> None:
"""
This is notification from a playlist tab that it holds the next
track to be played.
Actions required:
- Clear next track if on other tab
- Reset tab colour if on other tab
- Note next playlist tab
- Set next playlist_tab tab colour
- Note next track
- Update headers
- Populate info tabs
"""
# ***kae
return
# Clear next track if on another tab
if self.next_track_playlist_tab != playlist_tab:
# We need to reset the ex-next-track playlist
if self.next_track_playlist_tab:
self.next_track_playlist_tab.clear_next(session)
# Reset tab colour if on other tab
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(
self.next_track_playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
# Note next playlist tab
self.next_track_playlist_tab = playlist_tab
# Set next playlist_tab tab colour if it isn't the
# currently-playing tab
if (self.next_track_playlist_tab !=
self.current_track_playlist_tab):
self.set_tab_colour(
self.next_track_playlist_tab,
QColor(Config.COLOUR_NEXT_TAB))
# Note next track
self.next_track = TrackData(track)
# Update headers
self.update_headers()
# Populate 'info' tabs
self.open_info_tabs()
#
# def tick(self) -> None:
# """

View File

@ -15,9 +15,9 @@ from PyQt5.QtWidgets import (
# QInputDialog,
# QLineEdit,
QMainWindow,
# QMenu,
QMenu,
# QStyledItemDelegate,
# QMessageBox,
QMessageBox,
QTableWidget,
QTableWidgetItem,
)
@ -25,14 +25,14 @@ from PyQt5.QtWidgets import (
import helpers
# import os
import re
# import subprocess
# import threading
import subprocess
import threading
#
from config import Config
from datetime import datetime # , timedelta
from helpers import (
get_relative_date,
# open_in_audacity
open_in_audacity
)
from log import log
from models import (
@ -102,7 +102,7 @@ class PlaylistTab(QTableWidget):
self.musicmuster: QMainWindow = musicmuster
self.playlist_id: int = playlist_id
# self.menu: Optional[QMenu] = None
self.menu: Optional[QMenu] = None
# self.current_track_start_time: Optional[datetime] = None
#
# # Don't select text on edit
@ -293,7 +293,7 @@ class PlaylistTab(QTableWidget):
# Rescan
act_rescan = self.menu.addAction("Rescan")
act_rescan.triggered.connect(
lambda: self._rescan(track_id)
lambda: self._rescan(row_number, track_id)
)
self.menu.addSeparator()
@ -557,7 +557,7 @@ class PlaylistTab(QTableWidget):
# self._set_row_content(row, track.id)
#
# # Mark track if file is unreadable
# if not self._file_is_readable(track.path):
# if not helpers.file_is_readable(track.path):
# self._set_unreadable_row(row)
# # Scroll to new row
# self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter)
@ -1242,10 +1242,13 @@ class PlaylistTab(QTableWidget):
# """Clear played status on row"""
#
# self._meta_clear_attribute(row, RowMeta.PLAYED)
#
# def _delete_rows(self) -> None:
# """Delete mutliple rows"""
#
def _delete_rows(self) -> None:
"""Delete mutliple rows"""
# ***KAE
pass
# log.debug("playlist._delete_rows()")
#
# selected_rows: List[int] = sorted(
@ -1444,18 +1447,18 @@ class PlaylistTab(QTableWidget):
# notes_rows: Set[int] = set(self._get_notes_rows())
#
# return list(unplayed_rows - notes_rows)
#
# def _get_row_start_time(self, row: int) -> Optional[datetime]:
# try:
# if self.item(row, self.COL_START_TIME):
# return datetime.strptime(self.item(
# row, self.COL_START_TIME).text(),
# Config.NOTE_TIME_FORMAT
# )
# else:
# return None
# except ValueError:
# return None
def _get_row_start_time(self, row: int) -> Optional[datetime]:
try:
if self.item(row, columns['start_time'].idx):
return datetime.strptime(self.item(
row, columns['start_time'].idx).text(),
Config.NOTE_TIME_FORMAT
)
else:
return None
except ValueError:
return None
#
# def _get_row_track_object(self, row: int, session: Session) \
# -> Optional[Tracks]:
@ -1578,15 +1581,15 @@ class PlaylistTab(QTableWidget):
# new_metadata: int = self._meta_get(row) & ~(1 << attribute)
# self.item(row, self.COL_USERDATA).setData(
# self.ROW_FLAGS, new_metadata)
#
# def _meta_clear_next(self) -> None:
# """
# Clear next row if there is one.
# """
#
# next_row: Optional[int] = self._get_next_track_row()
# if next_row is not None:
# self._meta_clear_attribute(next_row, RowMeta.NEXT)
def _meta_clear_next(self) -> None:
"""
Clear next row if there is one.
"""
next_row: Optional[int] = self._get_next_track_row()
if next_row is not None:
self._meta_clear_attribute(next_row, RowMeta.NEXT)
def _meta_get(self, row: int) -> int:
"""Return row metadata"""
@ -1637,20 +1640,20 @@ class PlaylistTab(QTableWidget):
f"in rows: {', '.join([str(x) for x in matches])}"
)
raise AttributeError(f"Multiple '{metadata}' metadata {matches}")
#
# def _meta_set_attribute(self, row: int, attribute: int) -> None:
# """Set row metadata"""
#
# if row is None:
# raise ValueError(f"_meta_set_attribute({row=}, {attribute=})")
#
# current_metadata: int = self._meta_get(row)
# if not current_metadata:
# new_metadata: int = (1 << attribute)
# else:
# new_metadata = self._meta_get(row) | (1 << attribute)
# self.item(row, self.COL_USERDATA).setData(
# self.ROW_FLAGS, new_metadata)
def _meta_set_attribute(self, row: int, attribute: int) -> None:
"""Set row metadata"""
if row is None:
raise ValueError(f"_meta_set_attribute({row=}, {attribute=})")
current_metadata: int = self._meta_get(row)
if not current_metadata:
new_metadata: int = (1 << attribute)
else:
new_metadata = self._meta_get(row) | (1 << attribute)
self.item(row, columns['userdata'].idx).setData(
self.ROW_FLAGS, new_metadata)
def _mplayer_play(self, track_id: int) -> None:
"""Play track with mplayer"""
@ -1673,21 +1676,30 @@ class PlaylistTab(QTableWidget):
"""Remove track from row, making it a section header"""
# Update playlist_rows record
plr = session.get(PlaylistRows, self._get_playlistrow_id(row))
plr.track_id = None
plr.save()
with Session() as session:
plr = session.get(PlaylistRows, self._get_playlistrow_id(row))
plr.track_id = None
# We can't have null text
if not plr.note:
plr.note = Config.TEXT_NO_TRACK_NO_NOTE
session.commit()
# Clear track text items
for i in range(2, len(columns) - 1):
self.item(row, i).setText("")
# Set note text in correct column for section head
self.item(row, 1).setText(plr.note)
# Remove row duration
self._set_row_duration(row, 0)
# And refresh display
self.update_display()
# Clear track text items
for i in range(2, len(columns)):
self.item(row, i).setText("")
# Set note text in correct column for section head
self.item(row, 1).setText(plr.note)
# Remove row duration
self._set_row_duration(row, 0)
# Remote track_id from row
self.item(row, columns['userdata'].idx).setData(
self.ROW_TRACK_ID, 0)
# Span the rows
self.setSpan(row, 1, 1, len(columns))
# And refresh display
self.update_display(session)
def _rescan(self, track_id: int) -> None:
def _rescan(self, row: int, track_id: int) -> None:
"""Rescan track"""
with Session() as session:
@ -1699,13 +1711,13 @@ class PlaylistTab(QTableWidget):
)
return
track.rescan(session)
self._update_row(session, row, track)
track.rescan(session)
self._update_row(session, row, track)
# def _run_subprocess(self, args):
# """Run args in subprocess"""
#
# subprocess.call(args)
def _run_subprocess(self, args):
"""Run args in subprocess"""
subprocess.call(args)
#
# def _set_current_track_row(self, row: int) -> None:
# """Mark this row as current track"""
@ -1796,7 +1808,7 @@ class PlaylistTab(QTableWidget):
else:
self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH)
def _set_next(self, session: Session, row: int) -> None:
def _set_next(self, session: Session, row_number: int) -> None:
"""
Set passed row as next track to play.
@ -1810,21 +1822,23 @@ class PlaylistTab(QTableWidget):
track_id = self._get_row_track_id(row_number)
if not track_id:
log.error(f"playlists._set_next({row=}) has no track associated")
log.error(
f"playlists._set_next({row_number=}) has no track associated"
)
return
track = session.get(Tracks, track_id)
if not track:
log.error(f"playlists._set_next({row=}): Track not found")
log.error(f"playlists._set_next({row_number=}): Track not found")
return
# Check track is readable
if not self._file_is_readable(track.path):
self._set_unreadable_row(row)
if not helpers.file_is_readable(track.path):
self._set_unreadable_row(row_number)
return None
# Mark as next track
self._set_next_track_row(row)
self._set_next_track_row(row_number)
# Update display
self.update_display(session)
@ -1904,28 +1918,27 @@ class PlaylistTab(QTableWidget):
# display_text = note_text + ' [' + duration + caveat + ']'
# item = self.item(start_row, self.COL_TITLE)
# item.setText(display_text)
#
# def _update_row(self, session, row: int, track: Tracks) -> None:
# """
# Update the passed row with info from the passed track.
# """
#
# log.debug(f"_update_row({row=}, {track=}")
#
# item_startgap: QTableWidgetItem = self.item(row, self.COL_MSS)
# item_startgap.setText(str(track.start_gap))
# if track.start_gap >= 500:
# item_startgap.setBackground(QColor(Config.COLOUR_LONG_START))
# else:
# item_startgap.setBackground(QColor("white"))
#
# item_title: QTableWidgetItem = self.item(row, self.COL_TITLE)
# item_title.setText(track.title)
#
# item_artist: QTableWidgetItem = self.item(row, self.COL_ARTIST)
# item_artist.setText(track.artist)
#
# item_duration: QTableWidgetItem = self.item(row, self.COL_DURATION)
# item_duration.setText(helpers.ms_to_mmss(track.duration))
#
# self.update_display(session)
def _update_row(self, session, row: int, track: Tracks) -> None:
"""
Update the passed row with info from the passed track.
"""
columns['start_time'].idx
item_startgap = self.item(row, columns['start_gap'].idx)
item_startgap.setText(str(track.start_gap))
if track.start_gap >= 500:
item_startgap.setBackground(QColor(Config.COLOUR_LONG_START))
else:
item_startgap.setBackground(QColor("white"))
item_title = self.item(row, columns['title'].idx)
item_title.setText(track.title)
item_artist = self.item(row, columns['artist'].idx)
item_artist.setText(track.artist)
item_duration = self.item(row, columns['duration'].idx)
item_duration.setText(helpers.ms_to_mmss(track.duration))
self.update_display(session)

11
poetry.lock generated
View File

@ -358,6 +358,14 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pydub-stubs"
version = "0.25.1.0"
description = "Stub-only package containing type information for pydub"
category = "dev"
optional = false
python-versions = ">=3.8,<4.0"
[[package]]
name = "pygments"
version = "2.11.2"
@ -614,7 +622,7 @@ python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "32b91fc8cb421cc92689db4fc1a4647044714d6e2a72194fe7caf2e25c821b55"
content-hash = "7754808d801630b110a46869b849a6ce205784f587d3c1d4ed2097553e4368c4"
[metadata.files]
alembic = [
@ -886,6 +894,7 @@ pydub = [
{file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"},
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
]
pydub-stubs = []
pygments = [
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
{file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},

View File

@ -26,6 +26,7 @@ PyQt5-stubs = "^5.15.2"
mypy = "^0.931"
pytest = "^7.0.1"
pytest-qt = "^4.0.2"
pydub-stubs = "^0.25.1"
[build-system]
requires = ["poetry-core>=1.0.0"]