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

View File

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

View File

@ -2,7 +2,8 @@
import logging import logging
import logging.handlers import logging.handlers
import stackprinter import os
import stackprinter # type: ignore
import sys import sys
import traceback import traceback
@ -55,40 +56,30 @@ syslog.addFilter(local_filter)
stderr.addFilter(local_filter) stderr.addFilter(local_filter)
stderr.addFilter(debug_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(stderr)
log.addHandler(syslog) log.addHandler(syslog)
def log_uncaught_exceptions(ex_cls, ex, tb): def log_uncaught_exceptions(_ex_cls, ex, tb):
from helpers import send_mail from helpers import send_mail
print("\033[1;31;47m") print("\033[1;31;47m")
logging.critical(''.join(traceback.format_tb(tb))) logging.critical(''.join(traceback.format_tb(tb)))
print("\033[1;37;40m") print("\033[1;37;40m")
stackprinter.show(style="lightbg") print(stackprinter.format(ex, style="darkbg2", add_summary=True))
msg = stackprinter.format(ex) if os.environ["MM_ENV"] != "DEVELOPMENT":
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, msg = stackprinter.format(ex)
"Exception from musicmuster", msg) send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
"Exception from musicmuster", msg)
sys.excepthook = log_uncaught_exceptions sys.excepthook = log_uncaught_exceptions

View File

@ -1,16 +1,16 @@
#!/usr/bin/python3 #!/usr/bin/python3
#
import os.path import os.path
import re import re
# import stackprinter # type: ignore
from dbconfig import Session from dbconfig import Session
#
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
#
# from pydub import AudioSegment
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
# from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
Column, Column,
@ -22,22 +22,16 @@ from sqlalchemy import (
Integer, Integer,
select, select,
String, String,
UniqueConstraint,
update, update,
) )
# from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import ( from sqlalchemy.orm import (
backref,
declarative_base, declarative_base,
relationship, relationship,
RelationshipProperty
) )
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.exc import ( from sqlalchemy.orm.exc import (
# MultipleResultsFound,
NoResultFound NoResultFound
) )
#
from config import Config from config import Config
from helpers import ( from helpers import (
fade_point, fade_point,
@ -47,7 +41,6 @@ from helpers import (
trailing_silence, trailing_silence,
) )
from log import log from log import log
#
Base = declarative_base() Base = declarative_base()
@ -69,7 +62,7 @@ class Carts(Base):
) )
def __init__(self, session: Session, cart_number: int, name: str = None, def __init__(self, session: Session, cart_number: int, name: str = None,
duration: int = None, path: str = None, duration: int = None, path: str = None,
enabled: bool = True) -> None: enabled: bool = True) -> None:
"""Create new cart""" """Create new cart"""
@ -82,23 +75,6 @@ class Carts(Base):
session.add(self) session.add(self)
session.commit() 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): class NoteColours(Base):
__tablename__ = 'notecolours' __tablename__ = 'notecolours'
@ -287,8 +263,17 @@ class Playlists(Base):
def close(self, session: Session) -> None: def close(self, session: Session) -> None:
"""Mark playlist as unloaded""" """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 self.tab = None
session.execute(
update(Playlists)
.where(Playlists.tab > closed_idx)
.values(tab=Playlists.tab - 1)
)
@classmethod @classmethod
def create_playlist_from_template(cls, def create_playlist_from_template(cls,
session: Session, session: Session,
@ -417,7 +402,7 @@ class PlaylistRows(Base):
return ( return (
f"<PlaylistRow(id={self.id}, playlist_id={self.playlist_id}, " f"<PlaylistRow(id={self.id}, playlist_id={self.playlist_id}, "
f"track_id={self.track_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, def __init__(self,
@ -452,41 +437,19 @@ class PlaylistRows(Base):
plr.note) plr.note)
@staticmethod @staticmethod
def delete_higher_rows(session: Session, playlist_id: int, row: int) \ def delete_plrids_not_in_list(session: Session, playlist_id: int,
-> None: plrids: List["PlaylistRows"]) -> None:
""" """
Delete rows in given playlist that have a higher row number Delete rows in given playlist that have a higher row number
than 'row' than 'maxrow'
"""
# 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
""" """
session.execute( session.execute(
delete(PlaylistRows) 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() # Delete won't take effect until commit()
session.commit() session.commit()
@ -509,6 +472,29 @@ class PlaylistRows(Base):
# Ensure new row numbers are available to the caller # Ensure new row numbers are available to the caller
session.commit() 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 @classmethod
def get_played_rows(cls, session: Session, def get_played_rows(cls, session: Session,
playlist_id: int) -> List[int]: playlist_id: int) -> List[int]:
@ -547,15 +533,6 @@ class PlaylistRows(Base):
return plrs 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 @classmethod
def get_unplayed_rows(cls, session: Session, def get_unplayed_rows(cls, session: Session,
playlist_id: int) -> List[int]: playlist_id: int) -> List[int]:
@ -593,6 +570,26 @@ class PlaylistRows(Base):
.values(row_number=PlaylistRows.row_number + move_by) .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): class Settings(Base):
"""Manage settings""" """Manage settings"""

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import re import re
import stackprinter import stackprinter # type: ignore
import subprocess import subprocess
import threading import threading
@ -34,7 +34,6 @@ from PyQt5.QtWidgets import (
QStyledItemDelegate, QStyledItemDelegate,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
QTextEdit,
QWidget QWidget
) )
@ -65,8 +64,6 @@ MINIMUM_ROW_HEIGHT = 30
class RowMeta: class RowMeta:
CLEAR = 0
NOTE = 1
UNREADABLE = 2 UNREADABLE = 2
NEXT = 3 NEXT = 3
CURRENT = 4 CURRENT = 4
@ -199,23 +196,13 @@ class PlaylistTab(QTableWidget):
# self.setSortingEnabled(True) # self.setSortingEnabled(True)
# Now load our tracks and notes # Now load our tracks and notes
self.populate(session, self.playlist_id) self.populate_display(session, self.playlist_id)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}>" return f"<PlaylistTab(id={self.playlist_id}>"
# ########## Events other than cell editing ########## # ########## 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: def dropEvent(self, event: QDropEvent) -> None:
""" """
Handle drag/drop of rows Handle drag/drop of rows
@ -327,8 +314,8 @@ class PlaylistTab(QTableWidget):
act_setnext.triggered.connect( act_setnext.triggered.connect(
lambda: self._set_next(session, row_number)) lambda: self._set_next(session, row_number))
# Open in Audacity
if not current: if not current:
# Open in Audacity
act_audacity = self.menu.addAction( act_audacity = self.menu.addAction(
"Open in Audacity") "Open in Audacity")
act_audacity.triggered.connect( act_audacity.triggered.connect(
@ -557,6 +544,17 @@ class PlaylistTab(QTableWidget):
self.clearSelection() self.clearSelection()
self.setDragEnabled(False) 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]: def get_selected_playlistrow_ids(self) -> Optional[List]:
""" """
Return a list of PlaylistRow ids of the selected rows Return a list of PlaylistRow ids of the selected rows
@ -584,63 +582,49 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing. to do the heavy lifing.
""" """
# PlaylistRows object requires a row number, but that number row_number = self.get_new_row_number()
# can be reset by calling PlaylistRows.fixup_rownumbers() later,
# so just fudge a row number for now.
row_number = 0
plr = PlaylistRows(session, self.playlist_id, None, row_number, note) plr = PlaylistRows(session, self.playlist_id, None, row_number, note)
self.insert_row(session, plr) self.insert_row(session, plr, repaint)
PlaylistRows.fixup_rownumbers(session, self.playlist_id) self.save_playlist(session)
if repaint:
self.update_display(session, clear_selection=False)
def insert_row(self, session: Session, row_data: PlaylistRows, def insert_row(self, session: Session, plr: PlaylistRows,
repaint: bool = True) -> None: repaint: bool = True) -> None:
""" """
Insert a row into playlist tab. Insert passed playlist row (plr) 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.
""" """
if self.selectionModel().hasSelection(): row = plr.row_number
row = self.currentRow()
else:
row = self.rowCount()
self.insertRow(row) self.insertRow(row)
# Add row metadata to userdata column # Add row metadata to userdata column
userdata_item = QTableWidgetItem() userdata_item = QTableWidgetItem()
userdata_item.setData(self.ROW_FLAGS, 0) userdata_item.setData(self.ROW_FLAGS, 0)
userdata_item.setData(self.PLAYLISTROW_ID, row_data.id) userdata_item.setData(self.PLAYLISTROW_ID, plr.id)
userdata_item.setData(self.ROW_TRACK_ID, row_data.track_id) userdata_item.setData(self.ROW_TRACK_ID, plr.track_id)
self.setItem(row, USERDATA, userdata_item) self.setItem(row, USERDATA, userdata_item)
if row_data.track_id: if plr.track_id:
# Add track details to items # Add track details to items
try: try:
start_gap = row_data.track.start_gap start_gap = plr.track.start_gap
except: except AttributeError:
return 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))
self.setItem(row, START_GAP, start_gap_item) 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()=}") log.debug(f"KAE: insert_row:619, {title_item.text()=}")
self.setItem(row, TITLE, title_item) 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) self.setItem(row, ARTIST, artist_item)
duration_item = QTableWidgetItem( duration_item = QTableWidgetItem(
ms_to_mmss(row_data.track.duration)) ms_to_mmss(plr.track.duration))
self.setItem(row, DURATION, duration_item) 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() start_item = QTableWidgetItem()
self.setItem(row, START_TIME, start_item) self.setItem(row, START_TIME, start_item)
@ -648,8 +632,8 @@ class PlaylistTab(QTableWidget):
end_item = QTableWidgetItem() end_item = QTableWidgetItem()
self.setItem(row, END_TIME, end_item) self.setItem(row, END_TIME, end_item)
if row_data.track.bitrate: if plr.track.bitrate:
bitrate = str(row_data.track.bitrate) bitrate = str(plr.track.bitrate)
else: else:
bitrate = "" bitrate = ""
bitrate_item = QTableWidgetItem(bitrate) bitrate_item = QTableWidgetItem(bitrate)
@ -657,23 +641,23 @@ class PlaylistTab(QTableWidget):
# As we have track info, any notes should be contained in # As we have track info, any notes should be contained in
# the notes column # the notes column
notes_item = QTableWidgetItem(row_data.note) notes_item = QTableWidgetItem(plr.note)
self.setItem(row, ROW_NOTES, notes_item) 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_str = get_relative_date(last_playtime)
last_played_item = QTableWidgetItem(last_played_str) last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, LASTPLAYED, last_played_item) self.setItem(row, LASTPLAYED, last_played_item)
# Mark track if file is unreadable # 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) self._set_unreadable_row(row)
else: else:
# This is a section header so it must have note text # This is a section header so it must have note text
if row_data.note is None: if plr.note is None:
log.debug( 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 return
@ -687,17 +671,16 @@ class PlaylistTab(QTableWidget):
continue continue
self.setItem(row, i, QTableWidgetItem()) self.setItem(row, i, QTableWidgetItem())
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1) 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) 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)
if repaint: if repaint:
self.save_playlist(session)
self.update_display(session, clear_selection=False) 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: note: str = None, repaint: bool = True) -> None:
""" """
Insert track into playlist tab. Insert track into playlist tab.
@ -709,20 +692,30 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing. to do the heavy lifing.
""" """
# PlaylistRows object requires a row number, but that number if not track and track.id:
# can be reset by calling PlaylistRows.fixup_rownumbers() later, log.debug(
# so just fudge a row number for now. f"insert_track({session=}, {track=}, {note=}, {repaint=}"
row_number = 0 " called with either no track or no track.id"
if track: )
track_id = track.id return
else:
track_id = None row_number = self.get_new_row_number()
plr = PlaylistRows(session, self.playlist_id,
track_id, row_number, note) # Check to see whether track is already in playlist
self.insert_row(session, plr) existing_plr = PlaylistRows.get_track_plr(session, track.id,
PlaylistRows.fixup_rownumbers(session, self.playlist_id) self.playlist_id)
if repaint: if existing_plr and ask_yes_no("Duplicate row",
self.update_display(session, clear_selection=False) "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: def play_started(self, session: Session) -> None:
""" """
@ -771,10 +764,10 @@ class PlaylistTab(QTableWidget):
self._clear_current_track_row() self._clear_current_track_row()
self.current_track_start_time = None 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: 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 # Sanity check row numbering before we load
@ -785,8 +778,8 @@ class PlaylistTab(QTableWidget):
# Add the rows # Add the rows
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
for row in playlist.rows: for plr in playlist.rows:
self.insert_row(session, row, repaint=False) self.insert_row(session, plr, repaint=False)
# Scroll to top # Scroll to top
if scroll_to_top: if scroll_to_top:
@ -822,22 +815,39 @@ class PlaylistTab(QTableWidget):
def save_playlist(self, session: Session) -> None: def save_playlist(self, session: Session) -> None:
""" """
All playlist rows have a PlaylistRows id. Check that that id points Get the PlaylistRow objects for each row in the display. Correct
to this playlist (in case track has been moved from other) and that the row_number and playlist_id if necessary. Remove any row
the row number is correct (in case tracks have been reordered). numbers in the database that are higher than the last row in
the display.
""" """
for row in range(self.rowCount()): # Build a dictionary of
plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) # {display_row_number: display_row_plr_id}
# Set the row number and playlist id (even if correct) display_plr_ids = {row_number: self._get_playlistrow_id(row_number)
plr.row_number = row for row_number in range(self.rowCount())}
plr.playlist_id = self.playlist_id
# Any rows in the database with a row_number higher that the # Now build a dictionary of
# current value of 'row' should not be there. Commit session # {display_row_number: display_row_plr}
# first to ensure any changes made above are committed. 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() 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: def scroll_current_to_top(self) -> None:
"""Scroll currently-playing row to top""" """Scroll currently-playing row to top"""
@ -860,65 +870,6 @@ class PlaylistTab(QTableWidget):
return return
self._search(next=True) 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: def search_next(self) -> None:
""" """
Select next row containg self.search_string. Select next row containg self.search_string.
@ -1009,12 +960,6 @@ class PlaylistTab(QTableWidget):
self.selectRow(row) 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: def set_selected_as_next(self) -> None:
"""Sets the select track as next to play""" """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( p.row_number for p in PlaylistRows.get_played_rows(
session, self.playlist_id) session, self.playlist_id)
] ]
unreadable: List[int] = self._get_unreadable_track_rows()
next_start_time = None next_start_time = None
section_start_plr = None section_start_plr = None
@ -1344,9 +1288,8 @@ class PlaylistTab(QTableWidget):
Delete mutliple rows Delete mutliple rows
Actions required: Actions required:
- Delete the rows from the PlaylistRows table
- Correct the row numbers in the PlaylistRows table
- Remove the rows from the display - Remove the rows from the display
- Save the playlist
""" """
# Delete rows from database # Delete rows from database
@ -1359,13 +1302,10 @@ class PlaylistTab(QTableWidget):
f"Really delete {row_count} row{plural}?"): f"Really delete {row_count} row{plural}?"):
return return
with Session() as session: self.remove_selected_rows()
PlaylistRows.delete_rows(session, plr_ids)
# Fix up row numbers left in this playlist with Session() as session:
PlaylistRows.fixup_rownumbers(session, self.playlist_id) QTimer.singleShot(0, lambda: self.save_playlist(session))
# Remove selected rows from display
self.remove_selected_rows()
def _drop_on(self, event): def _drop_on(self, event):
""" """
@ -1379,16 +1319,6 @@ class PlaylistTab(QTableWidget):
return (index.row() + 1 if self._is_below(event.pos(), index) return (index.row() + 1 if self._is_below(event.pos(), index)
else index.row()) 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, def _find_next_track_row(self, session: Session,
starting_row: int = None) -> Optional[int]: 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()])] [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: def _info_row(self, track_id: int) -> None:
"""Display popup with info re row""" """Display popup with info re row"""
@ -1658,6 +1578,20 @@ class PlaylistTab(QTableWidget):
new_metadata = self._meta_get(row) | (1 << attribute) new_metadata = self._meta_get(row) | (1 << attribute)
self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata) 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: def _mplayer_play(self, track_id: int) -> None:
"""Play track with mplayer""" """Play track with mplayer"""
@ -1769,6 +1703,65 @@ class PlaylistTab(QTableWidget):
scroll_item = self.item(top_row, 0) scroll_item = self.item(top_row, 0)
self.scrollToItem(scroll_item, QAbstractItemView.PositionAtTop) 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: def _select_event(self) -> None:
""" """
Called when item selection changes. Called when item selection changes.

View File

@ -4,7 +4,6 @@
# 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).
import glob
import os import os
import pydymenu # type: ignore import pydymenu # type: ignore
import shutil import shutil
@ -21,7 +20,6 @@ from helpers import (
from models import Tracks from models import Tracks
from dbconfig import Session from dbconfig import Session
from thefuzz import process # type: ignore
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from typing import List from typing import List
@ -35,15 +33,6 @@ source_dir = '/home/kae/music/Singles/tmp'
parent_dir = os.path.dirname(source_dir) 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] = [] name_and_tags: List[str] = []
tags_not_name: List[str] = [] tags_not_name: List[str] = []
multiple_similar: List[str] = [] multiple_similar: List[str] = []
@ -137,10 +126,6 @@ def main():
# Try to find a near match # 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: if process_no_matches:
prompt = f"\n file={new_fname}\n title={new_title}\n artist={new_artist}: " prompt = f"\n file={new_fname}\n title={new_title}\n artist={new_artist}: "
# Use fzf to search # Use fzf to search

View File

@ -16,20 +16,6 @@ from log import log
from models import Tracks 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): def check_db(session):
""" """
Database consistency check. Database consistency check.
@ -44,7 +30,7 @@ def check_db(session):
db_paths = set([a.path for a in Tracks.get_all(session)]) db_paths = set([a.path for a in Tracks.get_all(session)])
os_paths_list = [] 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: for f in files:
path = os.path.join(root, f) path = os.path.join(root, f)
ext = os.path.splitext(f)[1] ext = os.path.splitext(f)[1]