Compare commits

...

7 Commits

Author SHA1 Message Date
Keith Edmunds
f3ccab513b Put section headers in row 2
Bug in Qt means automatically setting row height doesn't take into
account row spans, so putting headers in narrow column makes for tall
rows.
2022-08-24 17:33:22 +01:00
Keith Edmunds
7819e863eb Merge branch 'EditorClosing' into v3_play 2022-08-24 14:35:10 +01:00
Keith Edmunds
9f6eb2554a close edit box with return 2022-08-24 14:35:01 +01:00
Keith Edmunds
b5c792b8d8 Lots of work on replace_files.py 2022-08-24 12:44:56 +01:00
Keith Edmunds
2b48e889a5 Always print summary from replace_files 2022-08-23 10:38:25 +01:00
Keith Edmunds
688267834d Set bitrate in replace_files.py 2022-08-23 09:32:26 +01:00
Keith Edmunds
c9a411d15d Tuning replace_files.py 2022-08-22 19:27:47 +01:00
4 changed files with 278 additions and 173 deletions

View File

@ -10,6 +10,7 @@ from PyQt5.QtCore import (
pyqtSignal, pyqtSignal,
QEvent, QEvent,
QModelIndex, QModelIndex,
QObject,
QSize, QSize,
Qt, Qt,
) )
@ -55,6 +56,7 @@ from models import (
) )
start_time_re = re.compile(r"@\d\d:\d\d:\d\d") start_time_re = re.compile(r"@\d\d:\d\d:\d\d")
HEADER_NOTES_COLUMN = 2
class RowMeta: class RowMeta:
@ -96,6 +98,16 @@ class NoSelectDelegate(QStyledItemDelegate):
return QPlainTextEdit(parent) return QPlainTextEdit(parent)
return super().createEditor(parent, option, index) return super().createEditor(parent, option, index)
def eventFilter(self, editor: QObject, event: QEvent):
"""By default, QPlainTextEdit doesn't handle enter or return"""
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Return:
if (Qt.ShiftModifier & event.modifiers()) != Qt.ShiftModifier:
self.commitData.emit(editor)
self.closeEditor.emit(editor)
return super().eventFilter(editor, event)
class PlaylistTab(QTableWidget): class PlaylistTab(QTableWidget):
# Qt.UserRoles # Qt.UserRoles
@ -213,7 +225,7 @@ class PlaylistTab(QTableWidget):
# rows. Check and fix: # rows. Check and fix:
for row in range(drop_row, drop_row + len(rows_to_move)): for row in range(drop_row, drop_row + len(rows_to_move)):
if not self._get_row_track_id(row): if not self._get_row_track_id(row):
self.setSpan(row, 1, 1, len(columns)) self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns))
# Scroll to drop zone # Scroll to drop zone
self.scrollToItem(self.item(row, 1)) self.scrollToItem(self.item(row, 1))
@ -358,7 +370,7 @@ class PlaylistTab(QTableWidget):
# #
# Call sequences: # Call sequences:
# Start editing: # Start editing:
# edit() (called twice; not sure why) # edit()
# _cell_edit_started() # _cell_edit_started()
# End editing: # End editing:
# _cell_changed() (only if changes made) # _cell_changed() (only if changes made)
@ -408,8 +420,8 @@ class PlaylistTab(QTableWidget):
editor: QWidget, editor: QWidget,
hint: QAbstractItemDelegate.EndEditHint) -> None: hint: QAbstractItemDelegate.EndEditHint) -> None:
""" """
Override QAbstractItemView.closeEditor to enable play controls Override PySide2.QAbstractItemView.closeEditor to enable
and update display. play controls and update display.
""" """
# update_display to update start times, such as when a note has # update_display to update start times, such as when a note has
@ -426,7 +438,7 @@ class PlaylistTab(QTableWidget):
trigger: QAbstractItemView.EditTrigger, trigger: QAbstractItemView.EditTrigger,
event: QEvent) -> bool: event: QEvent) -> bool:
""" """
Override QAbstractItemView.edit to catch when editing starts Override PySide2.QAbstractItemView.edit to catch when editing starts
""" """
result = super(PlaylistTab, self).edit(index, trigger, event) result = super(PlaylistTab, self).edit(index, trigger, event)
@ -450,16 +462,16 @@ class PlaylistTab(QTableWidget):
self.edit_cell_type = "row_notes" self.edit_cell_type = "row_notes"
else: else:
# Can't edit other columns # Can't edit other columns
return return False
# Check whether we're editing a notes row for later # Check whether we're editing a notes row for later
if self.edit_cell_type == "row_notes": if self.edit_cell_type == "row_notes":
note_column = columns['row_notes'].idx note_column = columns['row_notes'].idx
else: else:
# This is a section header. Text is always in row 1. # This is a section header.
if column != 1: if column != HEADER_NOTES_COLUMN:
return return False
note_column = 1 note_column = HEADER_NOTES_COLUMN
self.edit_cell_type = "row_notes" self.edit_cell_type = "row_notes"
# Disable play controls so that keyboard input doesn't # Disable play controls so that keyboard input doesn't
@ -561,7 +573,10 @@ class PlaylistTab(QTableWidget):
if row_data.track_id: if row_data.track_id:
# Add track details to items # Add track details to items
try:
start_gap = row_data.track.start_gap start_gap = row_data.track.start_gap
except:
return
start_gap_item = QTableWidgetItem(str(start_gap)) start_gap_item = QTableWidgetItem(str(start_gap))
if start_gap and start_gap >= 500: if start_gap and start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START)) start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
@ -615,12 +630,16 @@ class PlaylistTab(QTableWidget):
# Make empty items (row background won't be coloured without # Make empty items (row background won't be coloured without
# items present). Any notes should displayed starting in # items present). Any notes should displayed starting in
# column 0 # column 2 for now - bug in Qt means that when row size is
for i in range(2, len(columns)): # set, spanned columns are ignored, so put notes in col2
# (typically title).
for i in range(1, len(columns)):
if i == 2:
continue
self.setItem(row, i, QTableWidgetItem()) self.setItem(row, i, QTableWidgetItem())
self.setSpan(row, 1, 1, len(columns)) self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
notes_item = QTableWidgetItem(row_data.note) notes_item = QTableWidgetItem(row_data.note)
self.setItem(row, 1, notes_item) self.setItem(row, HEADER_NOTES_COLUMN, notes_item)
# Save (no) track_id # Save (no) track_id
userdata_item.setData(self.ROW_TRACK_ID, 0) userdata_item.setData(self.ROW_TRACK_ID, 0)
@ -1102,11 +1121,11 @@ class PlaylistTab(QTableWidget):
elif note_text.endswith("+"): elif note_text.endswith("+"):
section_start_plr = playlist_row section_start_plr = playlist_row
section_time = 0 section_time = 0
self._set_row_colour( self._set_row_colour(row, QColor(note_colour))
row, QColor(note_colour)
)
# Section headers are always bold # Section headers are always bold
self._set_row_bold(row) self._set_row_bold(row)
# Ensure content is visible by wrapping cells
self.resizeRowToContents(row)
continue continue
# Have we had a section start but not end? # Have we had a section start but not end?
@ -1369,7 +1388,7 @@ class PlaylistTab(QTableWidget):
if track_id: if track_id:
item_note = self.item(row, columns['row_notes'].idx) item_note = self.item(row, columns['row_notes'].idx)
else: else:
item_note = self.item(row, 1) item_note = self.item(row, HEADER_NOTES_COLUMN)
return item_note.text() return item_note.text()
def _get_row_start_time(self, row: int) -> Optional[datetime]: def _get_row_start_time(self, row: int) -> Optional[datetime]:
@ -1593,7 +1612,7 @@ class PlaylistTab(QTableWidget):
for i in range(2, len(columns)): for i in range(2, len(columns)):
self.item(row, i).setText("") self.item(row, i).setText("")
# Set note text in correct column for section head # Set note text in correct column for section head
self.item(row, 1).setText(plr.note) self.item(row, HEADER_NOTES_COLUMN).setText(plr.note)
# Remove row duration # Remove row duration
self._set_row_duration(row, 0) self._set_row_duration(row, 0)
# Remote track_id from row # Remote track_id from row
@ -1741,16 +1760,19 @@ class PlaylistTab(QTableWidget):
Set or reset row background colour Set or reset row background colour
""" """
j: int column: int
if colour: if colour:
brush = QBrush(colour) brush = QBrush(colour)
else: else:
brush = QBrush() brush = QBrush()
for j in range(1, self.columnCount()): for column in range(1, self.columnCount()):
if self.item(row, j): # Don't clear colour on start gap row
self.item(row, j).setBackground(brush) if not colour and column == columns['start_gap'].idx:
continue
if self.item(row, column):
self.item(row, column).setBackground(brush)
def _set_row_duration(self, row: int, ms: int) -> None: def _set_row_duration(self, row: int, ms: int) -> None:
"""Set duration of this row in row metadata""" """Set duration of this row in row metadata"""
@ -1808,12 +1830,12 @@ class PlaylistTab(QTableWidget):
additional_text: str) -> None: additional_text: str) -> None:
"""Append additional_text to row display""" """Append additional_text to row display"""
# Column to update is either 1 for a section header or the # Column to update is either HEADER_NOTES_COLUMN for a section
# appropriate row_notes column for a track row # header or the appropriate row_notes column for a track row
if playlist_row.track_id: if playlist_row.track_id:
column = columns['row_notes'].idx column = columns['row_notes'].idx
else: else:
column = 1 column = HEADER_NOTES_COLUMN
# Update text # Update text
new_text = playlist_row.note + additional_text new_text = playlist_row.note + additional_text

View File

@ -3,18 +3,10 @@
# Script to replace existing files in parent directory. Typical usage: # Script to replace existing files in parent directory. Typical usage:
# the current directory contains a "better" version of the file than the # the current directory contains a "better" version of the file than the
# parent (eg, bettet bitrate). # parent (eg, bettet bitrate).
#
# Actions:
#
# - check that the same filename is present in the parent directory
# - check that the artist and title tags are the same
# - append ".bak" to the version in the parent directory
# - move file to parent directory
# - normalise file
# - update duration, start_gap, fade_at, silence_at, mtime in database
import glob import glob
import os import os
import pydymenu # type: ignore
import shutil import shutil
import sys import sys
@ -33,182 +25,230 @@ from sqlalchemy.exc import IntegrityError
from typing import List from typing import List
# ###################### SETTINGS ######################### # ###################### SETTINGS #########################
process_multiple_matches = False process_name_and_tags_matches = True
do_processing = False process_tag_matches = True
process_no_matches = False do_processing = True
process_no_matches = True
source_dir = '/home/kae/music/Singles/tmp'
parent_dir = os.path.dirname(source_dir)
# ######################################################### # #########################################################
def insensitive_glob(pattern): def insensitive_glob(pattern):
"""Helper for case insensitive glob.glob()"""
def either(c): def either(c):
return '[%s%s]' % (c.lower(), c.upper()) if c.isalpha() else c return '[%s%s]' % (c.lower(), c.upper()) if c.isalpha() else c
return glob.glob(''.join(map(either, pattern))) return glob.glob(''.join(map(either, pattern)))
# Check file of same name exists in parent directory
source_dir = '/home/kae/music/Singles/tmp' # os.getcwd()
parent_dir = os.path.dirname(source_dir)
assert source_dir != parent_dir
name_and_tags: List[str] = [] name_and_tags: List[str] = []
name_not_tags: List[str] = []
tags_not_name: List[str] = [] tags_not_name: List[str] = []
multiple_similar: List[str] = [] multiple_similar: List[str] = []
no_match: List[str] = [] no_match: List[str] = []
possibles: List[str] = [] possibles: List[str] = []
no_match: int = 0
print(f"{source_dir=}, {parent_dir=}")
def main(): def main():
global no_match
# 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
if 'musicmuster_prod' not in os.environ.get('MM_DB'): if 'musicmuster_prod' not in os.environ.get('MM_DB'):
response = input("Not on production database - c to continue: ") response = input("Not on production database - c to continue: ")
if response != "c": if response != "c":
sys.exit(0) sys.exit(0)
tracks = os.listdir(parent_dir)
for fname in os.listdir(source_dir):
parent_file = os.path.join(parent_dir, fname)
new_file = os.path.join(source_dir, fname)
us = get_tags(new_file)
us_t = us['title']
us_a = us['artist']
if os.path.exists(parent_file): # Sanity check
# File exists, check tags assert source_dir != parent_dir
p = get_tags(parent_file)
p_t = p['title'] # Scan parent directory
p_a = p['artist'] with Session() as session:
all_tracks = Tracks.get_all(session)
parent_tracks = [a for a in all_tracks if parent_dir in a.path]
parent_fnames = [os.path.basename(a.path) for a in parent_tracks]
# Create a dictionary of parent paths with their titles and
# artists
parents = {}
for t in parent_tracks:
parents[t.path] = {"title": t.title, "artist": t.artist}
titles_to_path = {}
artists_to_path = {}
for k, v in parents.items():
try:
titles_to_path[v['title'].lower()] = k
artists_to_path[v['artist'].lower()] = k
except AttributeError:
continue
for new_fname in os.listdir(source_dir):
new_path = os.path.join(source_dir, new_fname)
new_tags = get_tags(new_path)
new_title = new_tags['title']
new_artist = new_tags['artist']
bitrate = new_tags['bitrate']
# If same filename exists in parent direcory, check tags
parent_path = os.path.join(parent_dir, new_fname)
if os.path.exists(parent_path):
parent_tags = get_tags(parent_path)
parent_title = parent_tags['title']
parent_artist = parent_tags['artist']
if ( if (
(str(p_t).lower() != str(us_t).lower()) or (str(parent_title).lower() == str(new_title).lower()) and
(str(p_a).lower() != str(us_a).lower()) (str(parent_artist).lower() == str(new_artist).lower())
): ):
name_not_tags.append( name_and_tags.append(
f" {fname=}, {p_t}{us_t}, {p_a}{us_a}") f" {new_fname=}, {parent_title}{new_title}, "
process_track(new_file, parent_file, us_t, us_a) f" {parent_artist}{new_artist}"
)
if process_name_and_tags_matches:
process_track(new_path, parent_path, new_title,
new_artist, bitrate)
continue continue
name_and_tags.append(new_file)
process_track(new_file, parent_file, us_t, us_a) # Check for matching tags although filename is different
if new_title.lower() in titles_to_path:
possible_path = titles_to_path[new_title.lower()]
if parents[possible_path]['artist'].lower() == new_artist.lower():
# print(
# f"title={new_title}, artist={new_artist}:\n"
# f" {new_path} → {parent_path}"
# )
tags_not_name.append(
f"title={new_title}, artist={new_artist}:\n"
f" {new_path}{parent_path}"
)
if process_tag_matches:
process_track(new_path, possible_path, new_title,
new_artist, bitrate)
continue continue
else:
no_match += 1
else:
no_match += 1
# Try to find a near match # Try to find a near match
stem = fname.split(".")[0]
matches = insensitive_glob(os.path.join(parent_dir, stem) + '*') # stem = new_fname.split(".")[0]
match_count = len(matches) # matches = insensitive_glob(os.path.join(parent_dir, stem) + '*')
if match_count == 0: # match_count = len(matches)
# if match_count == 0:
if process_no_matches: if process_no_matches:
print(f"\n file={fname}\n title={us_t}\n artist={us_a}\n") prompt = f"\n file={new_fname}\n title={new_title}\n artist={new_artist}: "
# Try fuzzy search # Use fzf to search
d = {} choice = pydymenu.fzf(parent_fnames, prompt)
while True: if choice:
for i, match in enumerate( old_file = os.path.join(parent_dir, choice[0])
[a[0] for a in process.extract(fname, tracks, limit=5)]
):
d[i] = match
for k, v in d.items():
print(f"{k}: {v}")
data = input("pick one, return to quit: ")
if data == "":
no_match.append(f"{fname}, {us_t=}, {us_a=}")
break
try:
key = int(data)
except ValueError:
continue
if key in d:
old_file = os.path.join(parent_dir, d[key])
oldtags = get_tags(old_file) oldtags = get_tags(old_file)
old_title = oldtags['title'] old_title = oldtags['title']
old_artist = oldtags['artist'] old_artist = oldtags['artist']
print() print()
print(f" Title tag will change {old_title}{us_t}") print(f" File name will change {choice[0]}")
print(f" Artist tag will change {old_artist}{us_a}") print(f"{new_fname}")
print()
print(f" Title tag will change {old_title}")
print(f"{new_title}")
print()
print(f" Artist tag will change {old_artist}")
print(f"{new_artist}")
print() print()
data = input("Go ahead (y to accept)? ") data = input("Go ahead (y to accept)? ")
if data == "y": if data == "y":
process_track(new_file, old_file, us_t, us_a) process_track(new_path, old_file, new_title, new_artist, bitrate)
break continue
if data == "q":
sys.exit(0)
else: else:
no_match.append(f"{fname}, {us_t=}, {us_a=}")
continue
no_match.append(f"{fname}, {us_t=}, {us_a=}")
continue
else:
no_match.append(f"{fname}, {us_t=}, {us_a=}")
continue continue
# else:
# no_match.append(f"{new_fname}, {new_title=}, {new_artist=}")
# continue
if match_count > 1: # if match_count > 1:
multiple_similar.append(fname + "\n " + "\n ".join(matches)) # multiple_similar.append(new_fname + "\n " + "\n ".join(matches))
if match_count <= 26 and process_multiple_matches: # if match_count <= 26 and process_multiple_matches:
print(f"\n file={fname}\n title={us_t}\n artist={us_a}\n") # print(f"\n file={new_fname}\n title={new_title}\n artist={new_artist}\n")
d = {} # d = {}
while True: # while True:
for i, match in enumerate(matches): # for i, match in enumerate(matches):
d[i] = match # d[i] = match
for k, v in d.items(): # for k, v in d.items():
print(f"{k}: {v}") # print(f"{k}: {v}")
data = input("pick one, return to quit: ") # data = input("pick one, return to quit: ")
if data == "": # if data == "":
break # break
try: # try:
key = int(data) # key = int(data)
except ValueError: # except ValueError:
continue # continue
if key in d: # if key in d:
dst = d[key] # dst = d[key]
process_track(new_file, dst, us_t, us_a) # process_track(new_path, dst, new_title, new_artist, bitrate)
break # break
else: # else:
continue # continue
continue # from break after testing for "" in data # continue # from break after testing for "" in data
# One match, check tags # # One match, check tags
sim_name = matches[0] # sim_name = matches[0]
p = get_tags(sim_name) # p = get_tags(sim_name)
p_t = p['title'] # parent_title = p['title']
p_a = p['artist'] # parent_artist = p['artist']
if ( # if (
(str(p_t).lower() != str(us_t).lower()) or # (str(parent_title).lower() != str(new_title).lower()) or
(str(p_a).lower() != str(us_a).lower()) # (str(parent_artist).lower() != str(new_artist).lower())
): # ):
possibles.append( # possibles.append(
f"File: {os.path.basename(sim_name)}{fname}" # f"File: {os.path.basename(sim_name)} → {new_fname}"
f"\n {p_t}{us_t}\n {p_a}{us_a}" # f"\n {parent_title} → {new_title}\n {parent_artist} → {new_artist}"
) # )
process_track(new_file, sim_name, us_t, us_a) # process_track(new_path, sim_name, new_title, new_artist, bitrate)
continue # continue
tags_not_name.append(f"Rename {os.path.basename(sim_name)}{fname}") # tags_not_name.append(f"Rename {os.path.basename(sim_name)} → {new_fname}")
process_track(new_file, sim_name, us_t, us_a) # process_track(new_path, sim_name, new_title, new_artist, bitrate)
print(f"Name and tags match ({len(name_and_tags)}):") print(f"Name and tags match ({len(name_and_tags)}):")
# print(" \n".join(name_and_tags)) # print(" \n".join(name_and_tags))
# print() # print()
print(f"Name but not tags match ({len(name_not_tags)}):") # print(f"Name but not tags match ({len(name_not_tags)}):")
print(" \n".join(name_not_tags)) # print(" \n".join(name_not_tags))
print() # print()
print(f"Tags but not name match ({len(tags_not_name)}):") print(f"Tags but not name match ({len(tags_not_name)}):")
# print(" \n".join(tags_not_name)) # print(" \n".join(tags_not_name))
# print() # print()
print(f"Multiple similar names ({len(multiple_similar)}):") # print(f"Multiple similar names ({len(multiple_similar)}):")
print(" \n".join(multiple_similar)) # print(" \n".join(multiple_similar))
print() # print()
print(f"Possibles: ({len(possibles)}):") # print(f"Possibles: ({len(possibles)}):")
print(" \n".join(possibles)) # print(" \n".join(possibles))
print() # print()
print(f"No match ({len(no_match)}):") # print(f"No match ({len(no_match)}):")
print(" \n".join(no_match)) # print(" \n".join(no_match))
print() # print()
# print(f"Name and tags match ({len(name_and_tags)}):")
# print(f"Name but not tags match ({len(name_not_tags)}):")
# print(f"Tags but not name match ({len(tags_not_name)}):")
# print(f"Multiple similar names ({len(multiple_similar)}):")
# print(f"Possibles: ({len(possibles)}):")
# print(f"No match ({len(no_match)}):")
print(f"No matches: {no_match}")
def process_track(src, dst, title, artist): def process_track(src, dst, title, artist, bitrate):
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src)) new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
print( print(
f"process_track:\n {src=}\n {new_path=}\n " f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n"
f"{dst=}\n {title=}, {artist=}\n"
) )
if not do_processing: if not do_processing:
@ -220,6 +260,7 @@ def process_track(src, dst, title, artist):
track.title = title track.title = title
track.artist = artist track.artist = artist
track.path = new_path track.path = new_path
track.bitrate = bitrate
try: try:
session.commit() session.commit()
except IntegrityError: except IntegrityError:
@ -228,6 +269,7 @@ def process_track(src, dst, title, artist):
track.title = title track.title = title
track.artist = artist track.artist = artist
track.path = "DUMMY" track.path = "DUMMY"
track.bitrate = bitrate
session.commit() session.commit()
track.path = new_path track.path = new_path
session.commit() session.commit()

44
poetry.lock generated
View File

@ -73,6 +73,17 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "commonmark"
version = "0.9.1"
description = "Python parser for the CommonMark Markdown spec"
category = "main"
optional = false
python-versions = "*"
[package.extras]
test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"]
[[package]] [[package]]
name = "decorator" name = "decorator"
version = "5.1.1" version = "5.1.1"
@ -366,6 +377,17 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.8,<4.0" python-versions = ">=3.8,<4.0"
[[package]]
name = "pydymenu"
version = "0.5.2"
description = "A pythonic wrapper interface for fzf, dmenu, and rofi."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
rich = "*"
[[package]] [[package]]
name = "pyfzf" name = "pyfzf"
version = "0.3.1" version = "0.3.1"
@ -378,7 +400,7 @@ python-versions = "*"
name = "pygments" name = "pygments"
version = "2.11.2" version = "2.11.2"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
@ -519,6 +541,21 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "rich"
version = "12.5.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
python-versions = ">=3.6.3,<4.0.0"
[package.dependencies]
commonmark = ">=0.9.0,<0.10.0"
pygments = ">=2.6.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -671,7 +708,7 @@ python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "b181eb743e8b6c9cb7e03c4db0bcef425fe410d2ec3c4c801ce20e448a26f166" content-hash = "91e055875df86707e1ce1544b1d29126265011d750897912daa37af3fe005498"
[metadata.files] [metadata.files]
alembic = [ alembic = [
@ -702,6 +739,7 @@ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
] ]
commonmark = []
decorator = [ decorator = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
@ -944,6 +982,7 @@ pydub = [
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
] ]
pydub-stubs = [] pydub-stubs = []
pydymenu = []
pyfzf = [] pyfzf = []
pygments = [ pygments = [
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
@ -1020,6 +1059,7 @@ python-vlc = [
{file = "python-vlc-3.0.16120.tar.gz", hash = "sha256:92f98fee088f72bd6d063b3b3312d0bd29b37e7ad65ddeb3a7303320300c2807"}, {file = "python-vlc-3.0.16120.tar.gz", hash = "sha256:92f98fee088f72bd6d063b3b3312d0bd29b37e7ad65ddeb3a7303320300c2807"},
{file = "python_vlc-3.0.16120-py3-none-any.whl", hash = "sha256:c409afb38fe9f788a663b4302ca583f31289ef0860ab2b1668da96bbe8f14bfc"}, {file = "python_vlc-3.0.16120-py3-none-any.whl", hash = "sha256:c409afb38fe9f788a663b4302ca583f31289ef0860ab2b1668da96bbe8f14bfc"},
] ]
rich = []
six = [ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},

View File

@ -22,6 +22,7 @@ python-slugify = "^6.1.2"
thefuzz = "^0.19.0" thefuzz = "^0.19.0"
python-Levenshtein = "^0.12.2" python-Levenshtein = "^0.12.2"
pyfzf = "^0.3.1" pyfzf = "^0.3.1"
pydymenu = "^0.5.2"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
ipdb = "^0.13.9" ipdb = "^0.13.9"