Right-click menu mostly working
Still to implement: - Move to playlist - Remove row
This commit is contained in:
parent
89781c0a94
commit
99409e8626
@ -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"
|
||||
|
||||
258
app/helpers.py
258
app/helpers.py
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
# """
|
||||
|
||||
207
app/playlists.py
207
app/playlists.py
@ -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
11
poetry.lock
generated
@ -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"},
|
||||
|
||||
@ -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"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user