Compare commits

..

12 Commits

Author SHA1 Message Date
Keith Edmunds
683e76f9a0 Update database correctly when tabs are closed 2022-12-24 20:24:27 +00:00
Keith Edmunds
abd6ad0a64 Fix to not sending stack dumps in development environment 2022-12-24 20:23:30 +00:00
Keith Edmunds
ea4d7693ef Don't send stackdumps by mail in DEVELOPMENT environment 2022-12-24 18:46:04 +00:00
Keith Edmunds
94b2f473e9 Cleanups from running vulture 2022-12-24 09:36:51 +00:00
Keith Edmunds
f2a27366d3 Fix deleting rows from playlist 2022-12-23 21:27:06 +00:00
Keith Edmunds
46f2b662f3 Copy/paste, insert track/header works 2022-12-23 20:52:18 +00:00
Keith Edmunds
647e7d478a Move rows works. 2022-12-23 20:37:21 +00:00
Keith Edmunds
444c3e4fb4 Remove rows from playlist works and db updates 2022-12-23 20:15:07 +00:00
Keith Edmunds
35b101a538 Tidy up saving database 2022-12-23 17:23:43 +00:00
Keith Edmunds
d3958db8a3 Fix crash if create new playlist is cancelled 2022-12-23 09:27:14 +00:00
Keith Edmunds
be4f19757c Improve performance of save_playlist 2022-12-22 17:41:46 +00:00
Keith Edmunds
784d036bb7 Finally(?) sort out stackprinter logging. 2022-12-21 15:06:10 +00:00
9 changed files with 306 additions and 352 deletions

View File

@ -4,11 +4,9 @@ from typing import List, Optional
class Config(object):
AUDACITY_COMMAND = "/usr/bin/audacity"
AUDIO_SEGMENT_CHUNK_SIZE = 10
BITRATE_LOW_THRESHOLD = 192
BITRATE_OK_THRESHOLD = 300
CHECK_AUDACITY_AT_STARTUP = True
CART_DIRECTORY = "/home/kae/radio/CartTracks"
CARTS_COUNT = 10
CARTS_HIDE = True
@ -21,19 +19,16 @@ class Config(object):
COLOUR_CART_PROGRESSBAR = "#000000"
COLOUR_CART_READY = "#ffc107"
COLOUR_CART_UNCONFIGURED = "#f2f2f2"
COLOUR_CURRENT_HEADER = "#d4edda"
COLOUR_CURRENT_PLAYLIST = "#7eca8f"
COLOUR_CURRENT_TAB = "#248f24"
COLOUR_ENDING_TIMER = "#dc3545"
COLOUR_EVEN_PLAYLIST = "#d9d9d9"
COLOUR_LONG_START = "#dc3545"
COLOUR_NEXT_HEADER = "#fff3cd"
COLOUR_NEXT_PLAYLIST = "#ffc107"
COLOUR_NEXT_TAB = "#b38600"
COLOUR_NORMAL_TAB = "#000000"
COLOUR_NOTES_PLAYLIST = "#b8daff"
COLOUR_ODD_PLAYLIST = "#f2f2f2"
COLOUR_PREVIOUS_HEADER = "#f8d7da"
COLOUR_UNREADABLE = "#dc3545"
COLOUR_WARNING_TIMER = "#ffc107"
COLUMN_NAME_ARTIST = "Artist"
@ -46,13 +41,10 @@ class Config(object):
COLUMN_NAME_NOTES = "Notes"
COLUMN_NAME_START_TIME = "Start"
COLUMN_NAME_TITLE = "Title"
DBFS_FADE = -12
DBFS_SILENCE = -50
DEBUG_FUNCTIONS: List[Optional[str]] = []
DEBUG_MODULES: List[Optional[str]] = ['dbconfig']
DEFAULT_COLUMN_WIDTH = 200
DEFAULT_IMPORT_DIRECTORY = "/home/kae/Nextcloud/tmp"
DEFAULT_OUTPUT_DIRECTORY = "/home/kae/music/Singles"
DISPLAY_SQL = False
ERRORS_FROM = ['noreply@midnighthax.com']
ERRORS_TO = ['kae@midnighthax.com']
@ -78,7 +70,6 @@ class Config(object):
ROOT = os.environ.get('ROOT') or "/home/kae/music"
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
@ -86,6 +77,3 @@ class Config(object):
VOLUME_VLC_DEFAULT = 75
VOLUME_VLC_DROP3db = 65
WEB_ZOOM_FACTOR = 1.2
config = Config

View File

@ -235,7 +235,7 @@ def normalise_track(path):
stats = os.stat(path)
try:
# Copy original file
fd, temp_path = tempfile.mkstemp()
_, temp_path = tempfile.mkstemp()
shutil.copyfile(path, temp_path)
except Exception as err:
log.debug(

View File

@ -2,7 +2,8 @@
import logging
import logging.handlers
import stackprinter
import os
import stackprinter # type: ignore
import sys
import traceback
@ -55,37 +56,27 @@ syslog.addFilter(local_filter)
stderr.addFilter(local_filter)
stderr.addFilter(debug_filter)
stderr_fmt = logging.Formatter('[%(asctime)s] %(leveltag)s: %(message)s',
datefmt='%H:%M:%S')
syslog_fmt = logging.Formatter(
'[%(name)s] %(module)s.%(funcName)s - %(leveltag)s: %(message)s'
)
stderr.setFormatter(stderr_fmt)
syslog.setFormatter(syslog_fmt)
class VerboseExceptionFormatter(logging.Formatter):
def formatException(self, exc_info):
msg = stackprinter.format(exc_info)
lines = msg.split('\n')
lines_indented = ["" + line + "\n" for line in lines]
msg_indented = "".join(lines_indented)
return msg_indented
stderr_fmt = '[%(asctime)s] %(leveltag)s: %(message)s'
stderr_formatter = VerboseExceptionFormatter(stderr_fmt, datefmt='%H:%M:%S')
stderr.setFormatter(stderr_formatter)
syslog_fmt = '[%(name)s] %(module)s.%(funcName)s - %(leveltag)s: %(message)s'
syslog_formatter = VerboseExceptionFormatter(syslog_fmt)
syslog.setFormatter(syslog_formatter)
# add the handlers to the log
log.addHandler(stderr)
log.addHandler(syslog)
def log_uncaught_exceptions(ex_cls, ex, tb):
def log_uncaught_exceptions(_ex_cls, ex, tb):
from helpers import send_mail
print("\033[1;31;47m")
logging.critical(''.join(traceback.format_tb(tb)))
print("\033[1;37;40m")
stackprinter.show(style="lightbg")
print(stackprinter.format(ex, style="darkbg2", add_summary=True))
if os.environ["MM_ENV"] != "DEVELOPMENT":
msg = stackprinter.format(ex)
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
"Exception from musicmuster", msg)

View File

@ -1,16 +1,16 @@
#!/usr/bin/python3
#
import os.path
import re
#
import stackprinter # type: ignore
from dbconfig import Session
#
from datetime import datetime
from typing import List, Optional
#
# from pydub import AudioSegment
from sqlalchemy.ext.associationproxy import association_proxy
# from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy import (
Boolean,
Column,
@ -22,22 +22,16 @@ from sqlalchemy import (
Integer,
select,
String,
UniqueConstraint,
update,
)
# from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import (
backref,
declarative_base,
relationship,
RelationshipProperty
)
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.exc import (
# MultipleResultsFound,
NoResultFound
)
#
from config import Config
from helpers import (
fade_point,
@ -47,7 +41,6 @@ from helpers import (
trailing_silence,
)
from log import log
#
Base = declarative_base()
@ -82,23 +75,6 @@ class Carts(Base):
session.add(self)
session.commit()
@classmethod
def get_or_create(cls, session: Session, cart_number: int) -> "Carts":
"""
Return cart with passed cart number, or create a record if
none exists.
"""
try:
return (
session.execute(
select(Carts)
.where(Carts.cart_number == cart_number)
).scalar_one()
)
except NoResultFound:
return Carts(session, cart_number)
class NoteColours(Base):
__tablename__ = 'notecolours'
@ -287,8 +263,17 @@ class Playlists(Base):
def close(self, session: Session) -> None:
"""Mark playlist as unloaded"""
# Closing this tab will mean all higher-number tabs have moved
# down by one
closed_idx = self.tab
self.tab = None
session.execute(
update(Playlists)
.where(Playlists.tab > closed_idx)
.values(tab=Playlists.tab - 1)
)
@classmethod
def create_playlist_from_template(cls,
session: Session,
@ -417,7 +402,7 @@ class PlaylistRows(Base):
return (
f"<PlaylistRow(id={self.id}, playlist_id={self.playlist_id}, "
f"track_id={self.track_id}, "
f"note={self.note} row_number={self.row_number}>"
f"note={self.note}, row_number={self.row_number}>"
)
def __init__(self,
@ -452,41 +437,19 @@ class PlaylistRows(Base):
plr.note)
@staticmethod
def delete_higher_rows(session: Session, playlist_id: int, row: int) \
-> None:
def delete_plrids_not_in_list(session: Session, playlist_id: int,
plrids: List["PlaylistRows"]) -> None:
"""
Delete rows in given playlist that have a higher row number
than 'row'
"""
# Log the rows to be deleted
rows_to_go = session.execute(
select(PlaylistRows)
.where(PlaylistRows.playlist_id == playlist_id,
PlaylistRows.row_number > row)
).scalars().all()
if not rows_to_go:
return
for row in rows_to_go:
log.debug(f"Should delete: {row}")
# If needed later:
# session.delete(row)
rows_to_go = session.execute(
select(PlaylistRows)
.where(PlaylistRows.playlist_id == playlist_id,
PlaylistRows.row_number > row)
).scalars().all()
@staticmethod
def delete_rows(session: Session, ids: List[int]) -> None:
"""
Delete passed ids
than 'maxrow'
"""
session.execute(
delete(PlaylistRows)
.where(PlaylistRows.id.in_(ids))
.where(
PlaylistRows.playlist_id == playlist_id,
PlaylistRows.id.not_in(plrids)
)
)
# Delete won't take effect until commit()
session.commit()
@ -509,6 +472,29 @@ class PlaylistRows(Base):
# Ensure new row numbers are available to the caller
session.commit()
@staticmethod
def get_track_plr(session: Session, track_id: int,
playlist_id: int) -> Optional["PlaylistRows"]:
"""Return first matching PlaylistRows object or None"""
return session.scalars(
select(PlaylistRows)
.where(
PlaylistRows.track_id == track_id,
PlaylistRows.playlist_id == playlist_id
)
.limit(1)
).first()
@staticmethod
def get_last_used_row(session: Session, playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows"""
return session.execute(
select(func.max(PlaylistRows.row_number))
.where(PlaylistRows.playlist_id == playlist_id)
).scalar_one()
@classmethod
def get_played_rows(cls, session: Session,
playlist_id: int) -> List[int]:
@ -547,15 +533,6 @@ class PlaylistRows(Base):
return plrs
@staticmethod
def get_last_used_row(session: Session, playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows"""
return session.execute(
select(func.max(PlaylistRows.row_number))
.where(PlaylistRows.playlist_id == playlist_id)
).scalar_one()
@classmethod
def get_unplayed_rows(cls, session: Session,
playlist_id: int) -> List[int]:
@ -593,6 +570,26 @@ class PlaylistRows(Base):
.values(row_number=PlaylistRows.row_number + move_by)
)
@staticmethod
def indexed_by_id(session: Session, plr_ids: List[int]) -> dict:
"""
Return a dictionary of playlist_rows indexed by their plr id from
the passed plr_id list.
"""
plrs = session.execute(
select(PlaylistRows)
.where(
PlaylistRows.id.in_(plr_ids)
)
).scalars().all()
result = {}
for plr in plrs:
result[plr.id] = plr
return result
class Settings(Base):
"""Manage settings"""

View File

@ -19,11 +19,8 @@ class Music:
"""
def __init__(self) -> None:
# self.current_track_start_time = None
# self.fading = 0
self.VLC = vlc.Instance()
self.player = None
# self.track_path = None
self.max_volume = Config.VOLUME_VLC_DEFAULT
def fade(self) -> None:
@ -109,7 +106,6 @@ class Music:
return None
status = -1
self.track_path = path
if Config.COLON_IN_PATH_FIX:
media = self.VLC.media_new_path(path)

View File

@ -2,7 +2,7 @@
from log import log
import argparse
import stackprinter
import stackprinter # type: ignore
import subprocess
import sys
import threading
@ -42,7 +42,6 @@ from models import (
)
from config import Config
from playlists import PlaylistTab
from sqlalchemy.orm.exc import DetachedInstanceError
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
from ui.dlg_search_database_ui import Ui_Dialog # type: ignore
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
@ -359,7 +358,7 @@ class Window(QMainWindow, Ui_MainWindow):
def close_tab(self, tab_index: int) -> None:
"""
Close active playlist tab unless it holds the curren or next track.
Close playlist tab unless it holds the current or next track.
Called from close_playlist_tab() or by clicking close button on tab.
"""
@ -376,6 +375,12 @@ class Window(QMainWindow, Ui_MainWindow):
"Can't close next track playlist", 5000)
return
# Record playlist as closed and update remaining playlist tabs
with Session() as session:
playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
playlist = session.get(Playlists, playlist_id)
playlist.close(session)
# Close playlist and remove tab
self.tabPlaylist.widget(tab_index).close()
self.tabPlaylist.removeTab(tab_index)
@ -421,8 +426,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnStop.clicked.connect(self.stop)
self.hdrCurrentTrack.clicked.connect(self.show_current)
self.hdrNextTrack.clicked.connect(self.show_next)
self.tabPlaylist.currentChanged.connect(
lambda: self.tabPlaylist.currentWidget().tab_visible())
self.tabPlaylist.currentChanged.connect(self.tab_change)
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
self.tabBar = self.tabPlaylist.tabBar()
self.tabBar.tabMoved.connect(self.move_tab)
@ -437,6 +441,8 @@ class Window(QMainWindow, Ui_MainWindow):
if not playlist_name:
playlist_name = self.solicit_playlist_name()
if not playlist_name:
return
playlist = Playlists(session, playlist_name)
return playlist
@ -446,6 +452,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session:
playlist = self.create_playlist(session)
if playlist:
self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session: Session,
@ -736,6 +743,7 @@ class Window(QMainWindow, Ui_MainWindow):
helpers.set_track_metadata(session, track)
helpers.normalise_track(track.path)
self.visible_playlist_tab().insert_track(session, track)
self.visible_playlist_tab().save_playlist(session)
def insert_header(self) -> None:
"""Show dialog box to enter header text and add to playlist"""
@ -755,6 +763,7 @@ class Window(QMainWindow, Ui_MainWindow):
if ok:
with Session() as session:
playlist_tab.insert_header(session, dlg.textValue())
playlist_tab.save_playlist(session)
def insert_track(self) -> None:
"""Show dialog box to select and add track from database"""
@ -792,6 +801,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Identify destination playlist
visible_tab = self.visible_playlist_tab()
source_playlist = visible_tab.playlist_id
# Get destination playlist id
playlists = []
for playlist in Playlists.get_all(session):
if playlist.id == source_playlist:
@ -799,17 +810,17 @@ class Window(QMainWindow, Ui_MainWindow):
else:
playlists.append(playlist)
# Get destination playlist id
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
dlg.exec()
if not dlg.playlist:
return
destination_playlist_id = dlg.playlist.id
# Remove moved rows from display
# Remove moved rows from display and save
visible_tab.remove_rows([plr.row_number for plr in playlistrows])
visible_tab.save_playlist(session)
# Update playlist for the rows in the database
# Update destination playlist in the database
last_row = PlaylistRows.get_last_used_row(session,
destination_playlist_id)
if last_row is not None:
@ -825,13 +836,13 @@ class Window(QMainWindow, Ui_MainWindow):
# Update destination playlist_tab if visible (if not visible, it
# will be re-populated when it is opened)
destionation_playlist_tab = None
destination_playlist_tab = None
for tab in range(self.tabPlaylist.count()):
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
destionation_playlist_tab = self.tabPlaylist.widget(tab)
destination_playlist_tab = self.tabPlaylist.widget(tab)
break
if destionation_playlist_tab:
destionation_playlist_tab.populate(session, dlg.playlist.id)
if destination_playlist_tab:
destination_playlist_tab.populate_display(session, dlg.playlist.id)
def move_selected(self) -> None:
"""
@ -913,17 +924,17 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_tab = self.visible_playlist_tab()
dst_playlist_id = playlist_tab.playlist_id
dst_row = self.visible_playlist_tab().get_new_row_number()
with Session() as session:
# Create space in destination playlist
if playlist_tab.selectionModel().hasSelection():
row = playlist_tab.currentRow()
PlaylistRows.move_rows_down(session, dst_playlist_id,
row, len(self.selected_plrs))
dst_row, len(self.selected_plrs))
session.commit()
# Update plrs
row = dst_row
src_playlist_id = None
dst_row = row
for plr in self.selected_plrs:
# Update moved rows
session.add(plr)
@ -936,8 +947,8 @@ class Window(QMainWindow, Ui_MainWindow):
session.commit()
# Update display
self.visible_playlist_tab().populate(session, dst_playlist_id,
scroll_to_top=False)
self.visible_playlist_tab().populate_display(
session, dst_playlist_id, scroll_to_top=False)
# If source playlist is not destination playlist, fixup row
# numbers and update display
@ -952,8 +963,8 @@ class Window(QMainWindow, Ui_MainWindow):
source_playlist_tab = self.tabPlaylist.widget(tab)
break
if source_playlist_tab:
source_playlist_tab.populate(session, src_playlist_id,
scroll_to_top=False)
source_playlist_tab.populate_display(
session, src_playlist_id, scroll_to_top=False)
# Reset so rows can't be repasted
self.selected_plrs = None
@ -1032,7 +1043,6 @@ class Window(QMainWindow, Ui_MainWindow):
)
fade_at = self.current_track.fade_at
silence_at = self.current_track.silence_at
length = self.current_track.duration
self.label_fade_length.setText(
helpers.ms_to_mmss(silence_at - fade_at))
self.label_start_time.setText(
@ -1202,6 +1212,15 @@ class Window(QMainWindow, Ui_MainWindow):
# Run end-of-track actions
self.end_of_track_actions()
def tab_change(self):
"""Called when active tab changed"""
try:
self.tabPlaylist.currentWidget().tab_visible()
except AttributeError:
# May also be called when last tab is closed
pass
def this_is_the_next_track(self, session: Session,
playlist_tab: PlaylistTab,
track: Tracks) -> None:
@ -1497,8 +1516,8 @@ class DbDialog(QDialog):
self.parent().visible_playlist_tab().insert_track(
self.session, track, note=self.ui.txtNote.text())
# Commit session to get correct row numbers if more tracks added
self.session.commit()
# Save to database (which will also commit changes)
self.parent().visible_playlist_tab().save_playlist(self.session)
# Clear note field and select search text to make it easier for
# next search
self.ui.txtNote.clear()
@ -1584,7 +1603,6 @@ class SelectPlaylistDialog(QDialog):
self.ui.buttonBox.rejected.connect(self.close)
self.session = session
self.playlist = None
self.plid = None
record = Settings.get_int_settings(
self.session, "select_playlist_dialog_width")
@ -1663,6 +1681,6 @@ if __name__ == "__main__":
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
"Exception from musicmuster", msg)
print("\033[1;31;47mUnhandled exception starts\033[1;37;40m")
stackprinter.show(style="darkbg2")
print("\033[1;31;47mUnhandled exception ends\033[1;37;40m")
print("\033[1;31;47mUnhandled exception starts")
stackprinter.show(style="darkbg")
print("Unhandled exception ends\033[1;37;40m")

View File

@ -1,5 +1,5 @@
import re
import stackprinter
import stackprinter # type: ignore
import subprocess
import threading
@ -34,7 +34,6 @@ from PyQt5.QtWidgets import (
QStyledItemDelegate,
QTableWidget,
QTableWidgetItem,
QTextEdit,
QWidget
)
@ -65,8 +64,6 @@ MINIMUM_ROW_HEIGHT = 30
class RowMeta:
CLEAR = 0
NOTE = 1
UNREADABLE = 2
NEXT = 3
CURRENT = 4
@ -199,23 +196,13 @@ class PlaylistTab(QTableWidget):
# self.setSortingEnabled(True)
# Now load our tracks and notes
self.populate(session, self.playlist_id)
self.populate_display(session, self.playlist_id)
def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}>"
# ########## Events other than cell editing ##########
def closeEvent(self, event) -> None:
"""Handle closing playist tab"""
with Session() as session:
# Record playlist as closed
playlist = session.get(Playlists, self.playlist_id)
playlist.close(session)
event.accept()
def dropEvent(self, event: QDropEvent) -> None:
"""
Handle drag/drop of rows
@ -327,8 +314,8 @@ class PlaylistTab(QTableWidget):
act_setnext.triggered.connect(
lambda: self._set_next(session, row_number))
# Open in Audacity
if not current:
# Open in Audacity
act_audacity = self.menu.addAction(
"Open in Audacity")
act_audacity.triggered.connect(
@ -557,6 +544,17 @@ class PlaylistTab(QTableWidget):
self.clearSelection()
self.setDragEnabled(False)
def get_new_row_number(self) -> int:
"""
Return the selected row or the row count if no row selected
(ie, new row will be appended)
"""
if self.selectionModel().hasSelection():
return self.currentRow()
else:
return self.rowCount()
def get_selected_playlistrow_ids(self) -> Optional[List]:
"""
Return a list of PlaylistRow ids of the selected rows
@ -584,63 +582,49 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing.
"""
# PlaylistRows object requires a row number, but that number
# can be reset by calling PlaylistRows.fixup_rownumbers() later,
# so just fudge a row number for now.
row_number = 0
row_number = self.get_new_row_number()
plr = PlaylistRows(session, self.playlist_id, None, row_number, note)
self.insert_row(session, plr)
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
if repaint:
self.update_display(session, clear_selection=False)
self.insert_row(session, plr, repaint)
self.save_playlist(session)
def insert_row(self, session: Session, row_data: PlaylistRows,
def insert_row(self, session: Session, plr: PlaylistRows,
repaint: bool = True) -> None:
"""
Insert a row into playlist tab.
If playlist has a row selected, add new row above. Otherwise,
add to end of playlist.
Note: we ignore the row number in the PlaylistRows record. That is
used only to order the query that generates the records.
Insert passed playlist row (plr) into playlist tab.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
row = plr.row_number
self.insertRow(row)
# Add row metadata to userdata column
userdata_item = QTableWidgetItem()
userdata_item.setData(self.ROW_FLAGS, 0)
userdata_item.setData(self.PLAYLISTROW_ID, row_data.id)
userdata_item.setData(self.ROW_TRACK_ID, row_data.track_id)
userdata_item.setData(self.PLAYLISTROW_ID, plr.id)
userdata_item.setData(self.ROW_TRACK_ID, plr.track_id)
self.setItem(row, USERDATA, userdata_item)
if row_data.track_id:
if plr.track_id:
# Add track details to items
try:
start_gap = row_data.track.start_gap
except:
start_gap = plr.track.start_gap
except AttributeError:
return
start_gap_item = QTableWidgetItem(str(start_gap))
if start_gap and start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
self.setItem(row, START_GAP, start_gap_item)
title_item = QTableWidgetItem(row_data.track.title)
title_item = QTableWidgetItem(plr.track.title)
log.debug(f"KAE: insert_row:619, {title_item.text()=}")
self.setItem(row, TITLE, title_item)
artist_item = QTableWidgetItem(row_data.track.artist)
artist_item = QTableWidgetItem(plr.track.artist)
self.setItem(row, ARTIST, artist_item)
duration_item = QTableWidgetItem(
ms_to_mmss(row_data.track.duration))
ms_to_mmss(plr.track.duration))
self.setItem(row, DURATION, duration_item)
self._set_row_duration(row, row_data.track.duration)
self._set_row_duration(row, plr.track.duration)
start_item = QTableWidgetItem()
self.setItem(row, START_TIME, start_item)
@ -648,8 +632,8 @@ class PlaylistTab(QTableWidget):
end_item = QTableWidgetItem()
self.setItem(row, END_TIME, end_item)
if row_data.track.bitrate:
bitrate = str(row_data.track.bitrate)
if plr.track.bitrate:
bitrate = str(plr.track.bitrate)
else:
bitrate = ""
bitrate_item = QTableWidgetItem(bitrate)
@ -657,23 +641,23 @@ class PlaylistTab(QTableWidget):
# As we have track info, any notes should be contained in
# the notes column
notes_item = QTableWidgetItem(row_data.note)
notes_item = QTableWidgetItem(plr.note)
self.setItem(row, ROW_NOTES, notes_item)
last_playtime = Playdates.last_played(session, row_data.track.id)
last_playtime = Playdates.last_played(session, plr.track.id)
last_played_str = get_relative_date(last_playtime)
last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, LASTPLAYED, last_played_item)
# Mark track if file is unreadable
if not file_is_readable(row_data.track.path):
if not file_is_readable(plr.track.path):
self._set_unreadable_row(row)
else:
# This is a section header so it must have note text
if row_data.note is None:
if plr.note is None:
log.debug(
f"insert_row({row_data=}) with no track_id and no note"
f"insert_row({plr=}) with no track_id and no note"
)
return
@ -687,17 +671,16 @@ class PlaylistTab(QTableWidget):
continue
self.setItem(row, i, QTableWidgetItem())
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
notes_item = QTableWidgetItem(row_data.note)
notes_item = QTableWidgetItem(plr.note)
self.setItem(row, HEADER_NOTES_COLUMN, notes_item)
# Save (no) track_id
userdata_item.setData(self.ROW_TRACK_ID, 0)
if repaint:
self.save_playlist(session)
self.update_display(session, clear_selection=False)
def insert_track(self, session: Session, track: Optional[Tracks],
def insert_track(self, session: Session, track: Tracks,
note: str = None, repaint: bool = True) -> None:
"""
Insert track into playlist tab.
@ -709,20 +692,30 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing.
"""
# PlaylistRows object requires a row number, but that number
# can be reset by calling PlaylistRows.fixup_rownumbers() later,
# so just fudge a row number for now.
row_number = 0
if track:
track_id = track.id
else:
track_id = None
plr = PlaylistRows(session, self.playlist_id,
track_id, row_number, note)
self.insert_row(session, plr)
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
if repaint:
self.update_display(session, clear_selection=False)
if not track and track.id:
log.debug(
f"insert_track({session=}, {track=}, {note=}, {repaint=}"
" called with either no track or no track.id"
)
return
row_number = self.get_new_row_number()
# Check to see whether track is already in playlist
existing_plr = PlaylistRows.get_track_plr(session, track.id,
self.playlist_id)
if existing_plr and ask_yes_no("Duplicate row",
"Track already in playlist. "
"Move to new location?"):
# Yes it is and we shoudl reuse it
return self._move_row(session, existing_plr, row_number)
# Build playlist_row object
plr = PlaylistRows(session, self.playlist_id, track.id,
row_number, note)
self.insert_row(session, plr, repaint)
# Let display update, then save playlist
QTimer.singleShot(0, lambda: self.save_playlist(session))
def play_started(self, session: Session) -> None:
"""
@ -771,10 +764,10 @@ class PlaylistTab(QTableWidget):
self._clear_current_track_row()
self.current_track_start_time = None
def populate(self, session: Session, playlist_id: int,
def populate_display(self, session: Session, playlist_id: int,
scroll_to_top: bool = True) -> None:
"""
Populate from the associated playlist ID
Populate display from the associated playlist ID
"""
# Sanity check row numbering before we load
@ -785,8 +778,8 @@ class PlaylistTab(QTableWidget):
# Add the rows
playlist = session.get(Playlists, playlist_id)
for row in playlist.rows:
self.insert_row(session, row, repaint=False)
for plr in playlist.rows:
self.insert_row(session, plr, repaint=False)
# Scroll to top
if scroll_to_top:
@ -822,22 +815,39 @@ class PlaylistTab(QTableWidget):
def save_playlist(self, session: Session) -> None:
"""
All playlist rows have a PlaylistRows id. Check that that id points
to this playlist (in case track has been moved from other) and that
the row number is correct (in case tracks have been reordered).
Get the PlaylistRow objects for each row in the display. Correct
the row_number and playlist_id if necessary. Remove any row
numbers in the database that are higher than the last row in
the display.
"""
for row in range(self.rowCount()):
plr = session.get(PlaylistRows, self._get_playlistrow_id(row))
# Set the row number and playlist id (even if correct)
plr.row_number = row
plr.playlist_id = self.playlist_id
# Build a dictionary of
# {display_row_number: display_row_plr_id}
display_plr_ids = {row_number: self._get_playlistrow_id(row_number)
for row_number in range(self.rowCount())}
# Any rows in the database with a row_number higher that the
# current value of 'row' should not be there. Commit session
# first to ensure any changes made above are committed.
# Now build a dictionary of
# {display_row_number: display_row_plr}
plr_dict_by_id = PlaylistRows.indexed_by_id(session,
display_plr_ids.values())
# Finally a dictionary of
# {display_row_number: plr}
row_plr = {row_number: plr_dict_by_id[display_plr_ids[row_number]]
for row_number in range(self.rowCount())}
# Ensure all row plrs have correct row number and playlist_id
for row in range(self.rowCount()):
row_plr[row].row_number = row
row_plr[row].playlist_id = self.playlist_id
# Any rows in the database for this playlist that have a plr id
# that's not in the displayed playlist need to be deleted.
# Ensure changes flushed
session.commit()
PlaylistRows.delete_higher_rows(session, self.playlist_id, row)
PlaylistRows.delete_plrids_not_in_list(session, self.playlist_id,
display_plr_ids.values())
def scroll_current_to_top(self) -> None:
"""Scroll currently-playing row to top"""
@ -860,65 +870,6 @@ class PlaylistTab(QTableWidget):
return
self._search(next=True)
def _search(self, next: bool = True) -> None:
"""
Select next/previous row containg self.search_string. Start from
top selected row if there is one, else from top.
Wrap at last/first row.
"""
if not self.search_text:
return
selected_row = self._get_selected_row()
if next:
if selected_row is not None and selected_row < self.rowCount() - 1:
starting_row = selected_row + 1
else:
starting_row = 0
else:
if selected_row is not None and selected_row > 0:
starting_row = selected_row - 1
else:
starting_row = self.rowCount() - 1
wrapped = False
match_row = None
row = starting_row
needle = self.search_text.lower()
while True:
# Check for match in title, artist or notes
title = self._get_row_title(row)
if title and needle in title.lower():
match_row = row
break
artist = self._get_row_artist(row)
if artist and needle in artist.lower():
match_row = row
break
note = self._get_row_note(row)
if note and needle in note.lower():
match_row = row
break
if next:
row += 1
if wrapped and row >= starting_row:
break
if row >= self.rowCount():
row = 0
wrapped = True
else:
row -= 1
if wrapped and row <= starting_row:
break
if row < 0:
row = self.rowCount() - 1
wrapped = True
if match_row is not None:
self.selectRow(row)
def search_next(self) -> None:
"""
Select next row containg self.search_string.
@ -1009,12 +960,6 @@ class PlaylistTab(QTableWidget):
self.selectRow(row)
def set_searchtext(self, text: Optional[str]) -> None:
"""Set the search text and find first match"""
self.search_text = text
self._find_next_match()
def set_selected_as_next(self) -> None:
"""Sets the select track as next to play"""
@ -1054,7 +999,6 @@ class PlaylistTab(QTableWidget):
p.row_number for p in PlaylistRows.get_played_rows(
session, self.playlist_id)
]
unreadable: List[int] = self._get_unreadable_track_rows()
next_start_time = None
section_start_plr = None
@ -1344,9 +1288,8 @@ class PlaylistTab(QTableWidget):
Delete mutliple rows
Actions required:
- Delete the rows from the PlaylistRows table
- Correct the row numbers in the PlaylistRows table
- Remove the rows from the display
- Save the playlist
"""
# Delete rows from database
@ -1359,14 +1302,11 @@ class PlaylistTab(QTableWidget):
f"Really delete {row_count} row{plural}?"):
return
with Session() as session:
PlaylistRows.delete_rows(session, plr_ids)
# Fix up row numbers left in this playlist
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
# Remove selected rows from display
self.remove_selected_rows()
with Session() as session:
QTimer.singleShot(0, lambda: self.save_playlist(session))
def _drop_on(self, event):
"""
https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget
@ -1379,16 +1319,6 @@ class PlaylistTab(QTableWidget):
return (index.row() + 1 if self._is_below(event.pos(), index)
else index.row())
def _find_next_match(self) -> None:
"""
Find next match of search_text. Start at first highlighted row
if there is one, else from top of playlist.
"""
start_row = self._get_selected_row()
if start_row is None:
start_row = 0
def _find_next_track_row(self, session: Session,
starting_row: int = None) -> Optional[int]:
"""
@ -1540,16 +1470,6 @@ class PlaylistTab(QTableWidget):
[row for row in set([a.row() for a in self.selectedItems()])]
)
def _get_unreadable_track_rows(self) -> List[int]:
"""Return rows marked as unreadable, or None"""
return self._meta_search(RowMeta.UNREADABLE, one=False)
# def _header_click(self, index: int) -> None:
# """Handle playlist header click"""
# print(f"_header_click({index=})")
def _info_row(self, track_id: int) -> None:
"""Display popup with info re row"""
@ -1658,6 +1578,20 @@ class PlaylistTab(QTableWidget):
new_metadata = self._meta_get(row) | (1 << attribute)
self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata)
def _move_row(self, session: Session, plr: PlaylistRows,
new_row_number: int) -> None:
"""Move playlist row to new_row_number using parent copy/paste"""
# Remove source row
self.removeRow(plr.row_number)
# Fixup plr row number
if plr.row_number < new_row_number:
plr.row_number = new_row_number - 1
else:
plr.row_number = new_row_number
self.insert_row(session, plr)
self.save_playlist(session)
def _mplayer_play(self, track_id: int) -> None:
"""Play track with mplayer"""
@ -1769,6 +1703,65 @@ class PlaylistTab(QTableWidget):
scroll_item = self.item(top_row, 0)
self.scrollToItem(scroll_item, QAbstractItemView.PositionAtTop)
def _search(self, next: bool = True) -> None:
"""
Select next/previous row containg self.search_string. Start from
top selected row if there is one, else from top.
Wrap at last/first row.
"""
if not self.search_text:
return
selected_row = self._get_selected_row()
if next:
if selected_row is not None and selected_row < self.rowCount() - 1:
starting_row = selected_row + 1
else:
starting_row = 0
else:
if selected_row is not None and selected_row > 0:
starting_row = selected_row - 1
else:
starting_row = self.rowCount() - 1
wrapped = False
match_row = None
row = starting_row
needle = self.search_text.lower()
while True:
# Check for match in title, artist or notes
title = self._get_row_title(row)
if title and needle in title.lower():
match_row = row
break
artist = self._get_row_artist(row)
if artist and needle in artist.lower():
match_row = row
break
note = self._get_row_note(row)
if note and needle in note.lower():
match_row = row
break
if next:
row += 1
if wrapped and row >= starting_row:
break
if row >= self.rowCount():
row = 0
wrapped = True
else:
row -= 1
if wrapped and row <= starting_row:
break
if row < 0:
row = self.rowCount() - 1
wrapped = True
if match_row is not None:
self.selectRow(row)
def _select_event(self) -> None:
"""
Called when item selection changes.

View File

@ -4,7 +4,6 @@
# the current directory contains a "better" version of the file than the
# parent (eg, bettet bitrate).
import glob
import os
import pydymenu # type: ignore
import shutil
@ -21,7 +20,6 @@ from helpers import (
from models import Tracks
from dbconfig import Session
from thefuzz import process # type: ignore
from sqlalchemy.exc import IntegrityError
from typing import List
@ -35,15 +33,6 @@ source_dir = '/home/kae/music/Singles/tmp'
parent_dir = os.path.dirname(source_dir)
# #########################################################
def insensitive_glob(pattern):
"""Helper for case insensitive glob.glob()"""
def either(c):
return '[%s%s]' % (c.lower(), c.upper()) if c.isalpha() else c
return glob.glob(''.join(map(either, pattern)))
name_and_tags: List[str] = []
tags_not_name: List[str] = []
multiple_similar: List[str] = []
@ -137,10 +126,6 @@ def main():
# Try to find a near match
# stem = new_fname.split(".")[0]
# matches = insensitive_glob(os.path.join(parent_dir, stem) + '*')
# match_count = len(matches)
# if match_count == 0:
if process_no_matches:
prompt = f"\n file={new_fname}\n title={new_title}\n artist={new_artist}: "
# Use fzf to search

View File

@ -16,20 +16,6 @@ from log import log
from models import Tracks
def create_track(session, path, normalise=None):
"""
Create track in database from passed path.
Return track.
"""
track = Tracks(session, path)
set_track_metadata(session, track)
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
normalise_track(path)
def check_db(session):
"""
Database consistency check.
@ -44,7 +30,7 @@ def check_db(session):
db_paths = set([a.path for a in Tracks.get_all(session)])
os_paths_list = []
for root, dirs, files in os.walk(Config.ROOT):
for root, _dirs, files in os.walk(Config.ROOT):
for f in files:
path = os.path.join(root, f)
ext = os.path.splitext(f)[1]