Compare commits
No commits in common. "683e76f9a03f12a17d14a625ab69a14724753e3d" and "6a2bcfff195d13e20bad0ac1b0bd0e7aea80e4c1" have entirely different histories.
683e76f9a0
...
6a2bcfff19
@ -4,9 +4,11 @@ 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
|
||||
@ -19,16 +21,19 @@ 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"
|
||||
@ -41,10 +46,13 @@ 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']
|
||||
@ -70,6 +78,7 @@ 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
|
||||
@ -77,3 +86,6 @@ class Config(object):
|
||||
VOLUME_VLC_DEFAULT = 75
|
||||
VOLUME_VLC_DROP3db = 65
|
||||
WEB_ZOOM_FACTOR = 1.2
|
||||
|
||||
|
||||
config = Config
|
||||
|
||||
@ -235,7 +235,7 @@ def normalise_track(path):
|
||||
stats = os.stat(path)
|
||||
try:
|
||||
# Copy original file
|
||||
_, temp_path = tempfile.mkstemp()
|
||||
fd, temp_path = tempfile.mkstemp()
|
||||
shutil.copyfile(path, temp_path)
|
||||
except Exception as err:
|
||||
log.debug(
|
||||
|
||||
33
app/log.py
33
app/log.py
@ -2,8 +2,7 @@
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import stackprinter # type: ignore
|
||||
import stackprinter
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
@ -56,27 +55,37 @@ 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")
|
||||
print(stackprinter.format(ex, style="darkbg2", add_summary=True))
|
||||
if os.environ["MM_ENV"] != "DEVELOPMENT":
|
||||
stackprinter.show(style="lightbg")
|
||||
msg = stackprinter.format(ex)
|
||||
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
|
||||
"Exception from musicmuster", msg)
|
||||
|
||||
137
app/models.py
137
app/models.py
@ -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,16 +22,22 @@ 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,
|
||||
@ -41,6 +47,7 @@ from helpers import (
|
||||
trailing_silence,
|
||||
)
|
||||
from log import log
|
||||
#
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
@ -75,6 +82,23 @@ 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'
|
||||
@ -263,17 +287,8 @@ 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,
|
||||
@ -402,7 +417,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,
|
||||
@ -437,19 +452,41 @@ class PlaylistRows(Base):
|
||||
plr.note)
|
||||
|
||||
@staticmethod
|
||||
def delete_plrids_not_in_list(session: Session, playlist_id: int,
|
||||
plrids: List["PlaylistRows"]) -> None:
|
||||
def delete_higher_rows(session: Session, playlist_id: int, row: int) \
|
||||
-> None:
|
||||
"""
|
||||
Delete rows in given playlist that have a higher row number
|
||||
than 'maxrow'
|
||||
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
|
||||
"""
|
||||
|
||||
session.execute(
|
||||
delete(PlaylistRows)
|
||||
.where(
|
||||
PlaylistRows.playlist_id == playlist_id,
|
||||
PlaylistRows.id.not_in(plrids)
|
||||
)
|
||||
.where(PlaylistRows.id.in_(ids))
|
||||
)
|
||||
# Delete won't take effect until commit()
|
||||
session.commit()
|
||||
@ -472,29 +509,6 @@ 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]:
|
||||
@ -533,6 +547,15 @@ 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]:
|
||||
@ -570,26 +593,6 @@ 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"""
|
||||
|
||||
@ -19,8 +19,11 @@ 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:
|
||||
@ -106,6 +109,7 @@ class Music:
|
||||
return None
|
||||
|
||||
status = -1
|
||||
self.track_path = path
|
||||
|
||||
if Config.COLON_IN_PATH_FIX:
|
||||
media = self.VLC.media_new_path(path)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from log import log
|
||||
import argparse
|
||||
import stackprinter # type: ignore
|
||||
import stackprinter
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@ -42,6 +42,7 @@ 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
|
||||
@ -358,7 +359,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
def close_tab(self, tab_index: int) -> None:
|
||||
"""
|
||||
Close playlist tab unless it holds the current or next track.
|
||||
Close active playlist tab unless it holds the curren or next track.
|
||||
Called from close_playlist_tab() or by clicking close button on tab.
|
||||
"""
|
||||
|
||||
@ -375,12 +376,6 @@ 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)
|
||||
@ -426,7 +421,8 @@ 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(self.tab_change)
|
||||
self.tabPlaylist.currentChanged.connect(
|
||||
lambda: self.tabPlaylist.currentWidget().tab_visible())
|
||||
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
|
||||
self.tabBar = self.tabPlaylist.tabBar()
|
||||
self.tabBar.tabMoved.connect(self.move_tab)
|
||||
@ -441,8 +437,6 @@ 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
|
||||
@ -452,7 +446,6 @@ 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,
|
||||
@ -743,7 +736,6 @@ 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"""
|
||||
@ -763,7 +755,6 @@ 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"""
|
||||
@ -801,8 +792,6 @@ 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:
|
||||
@ -810,17 +799,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 and save
|
||||
# Remove moved rows from display
|
||||
visible_tab.remove_rows([plr.row_number for plr in playlistrows])
|
||||
visible_tab.save_playlist(session)
|
||||
|
||||
# Update destination playlist in the database
|
||||
# Update playlist for the rows in the database
|
||||
last_row = PlaylistRows.get_last_used_row(session,
|
||||
destination_playlist_id)
|
||||
if last_row is not None:
|
||||
@ -836,13 +825,13 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
# Update destination playlist_tab if visible (if not visible, it
|
||||
# will be re-populated when it is opened)
|
||||
destination_playlist_tab = None
|
||||
destionation_playlist_tab = None
|
||||
for tab in range(self.tabPlaylist.count()):
|
||||
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
|
||||
destination_playlist_tab = self.tabPlaylist.widget(tab)
|
||||
destionation_playlist_tab = self.tabPlaylist.widget(tab)
|
||||
break
|
||||
if destination_playlist_tab:
|
||||
destination_playlist_tab.populate_display(session, dlg.playlist.id)
|
||||
if destionation_playlist_tab:
|
||||
destionation_playlist_tab.populate(session, dlg.playlist.id)
|
||||
|
||||
def move_selected(self) -> None:
|
||||
"""
|
||||
@ -924,17 +913,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,
|
||||
dst_row, len(self.selected_plrs))
|
||||
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)
|
||||
@ -947,8 +936,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
session.commit()
|
||||
|
||||
# Update display
|
||||
self.visible_playlist_tab().populate_display(
|
||||
session, dst_playlist_id, scroll_to_top=False)
|
||||
self.visible_playlist_tab().populate(session, dst_playlist_id,
|
||||
scroll_to_top=False)
|
||||
|
||||
# If source playlist is not destination playlist, fixup row
|
||||
# numbers and update display
|
||||
@ -963,8 +952,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
source_playlist_tab = self.tabPlaylist.widget(tab)
|
||||
break
|
||||
if source_playlist_tab:
|
||||
source_playlist_tab.populate_display(
|
||||
session, src_playlist_id, scroll_to_top=False)
|
||||
source_playlist_tab.populate(session, src_playlist_id,
|
||||
scroll_to_top=False)
|
||||
|
||||
# Reset so rows can't be repasted
|
||||
self.selected_plrs = None
|
||||
@ -1043,6 +1032,7 @@ 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(
|
||||
@ -1212,15 +1202,6 @@ 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:
|
||||
@ -1516,8 +1497,8 @@ class DbDialog(QDialog):
|
||||
|
||||
self.parent().visible_playlist_tab().insert_track(
|
||||
self.session, track, note=self.ui.txtNote.text())
|
||||
# Save to database (which will also commit changes)
|
||||
self.parent().visible_playlist_tab().save_playlist(self.session)
|
||||
# Commit session to get correct row numbers if more tracks added
|
||||
self.session.commit()
|
||||
# Clear note field and select search text to make it easier for
|
||||
# next search
|
||||
self.ui.txtNote.clear()
|
||||
@ -1603,6 +1584,7 @@ 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")
|
||||
@ -1681,6 +1663,6 @@ if __name__ == "__main__":
|
||||
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM,
|
||||
"Exception from musicmuster", msg)
|
||||
|
||||
print("\033[1;31;47mUnhandled exception starts")
|
||||
stackprinter.show(style="darkbg")
|
||||
print("Unhandled exception ends\033[1;37;40m")
|
||||
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")
|
||||
|
||||
349
app/playlists.py
349
app/playlists.py
@ -1,5 +1,5 @@
|
||||
import re
|
||||
import stackprinter # type: ignore
|
||||
import stackprinter
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
@ -34,6 +34,7 @@ from PyQt5.QtWidgets import (
|
||||
QStyledItemDelegate,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTextEdit,
|
||||
QWidget
|
||||
)
|
||||
|
||||
@ -64,6 +65,8 @@ MINIMUM_ROW_HEIGHT = 30
|
||||
|
||||
|
||||
class RowMeta:
|
||||
CLEAR = 0
|
||||
NOTE = 1
|
||||
UNREADABLE = 2
|
||||
NEXT = 3
|
||||
CURRENT = 4
|
||||
@ -196,13 +199,23 @@ class PlaylistTab(QTableWidget):
|
||||
# self.setSortingEnabled(True)
|
||||
|
||||
# Now load our tracks and notes
|
||||
self.populate_display(session, self.playlist_id)
|
||||
self.populate(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
|
||||
@ -314,8 +327,8 @@ class PlaylistTab(QTableWidget):
|
||||
act_setnext.triggered.connect(
|
||||
lambda: self._set_next(session, row_number))
|
||||
|
||||
if not current:
|
||||
# Open in Audacity
|
||||
if not current:
|
||||
act_audacity = self.menu.addAction(
|
||||
"Open in Audacity")
|
||||
act_audacity.triggered.connect(
|
||||
@ -544,17 +557,6 @@ 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
|
||||
@ -582,49 +584,63 @@ class PlaylistTab(QTableWidget):
|
||||
to do the heavy lifing.
|
||||
"""
|
||||
|
||||
row_number = self.get_new_row_number()
|
||||
# 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
|
||||
plr = PlaylistRows(session, self.playlist_id, None, row_number, note)
|
||||
self.insert_row(session, plr, repaint)
|
||||
self.save_playlist(session)
|
||||
self.insert_row(session, plr)
|
||||
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
|
||||
if repaint:
|
||||
self.update_display(session, clear_selection=False)
|
||||
|
||||
def insert_row(self, session: Session, plr: PlaylistRows,
|
||||
def insert_row(self, session: Session, row_data: PlaylistRows,
|
||||
repaint: bool = True) -> None:
|
||||
"""
|
||||
Insert passed playlist row (plr) into playlist tab.
|
||||
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.
|
||||
"""
|
||||
|
||||
row = plr.row_number
|
||||
if self.selectionModel().hasSelection():
|
||||
row = self.currentRow()
|
||||
else:
|
||||
row = self.rowCount()
|
||||
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, plr.id)
|
||||
userdata_item.setData(self.ROW_TRACK_ID, plr.track_id)
|
||||
userdata_item.setData(self.PLAYLISTROW_ID, row_data.id)
|
||||
userdata_item.setData(self.ROW_TRACK_ID, row_data.track_id)
|
||||
self.setItem(row, USERDATA, userdata_item)
|
||||
|
||||
if plr.track_id:
|
||||
if row_data.track_id:
|
||||
# Add track details to items
|
||||
try:
|
||||
start_gap = plr.track.start_gap
|
||||
except AttributeError:
|
||||
start_gap = row_data.track.start_gap
|
||||
except:
|
||||
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(plr.track.title)
|
||||
title_item = QTableWidgetItem(row_data.track.title)
|
||||
log.debug(f"KAE: insert_row:619, {title_item.text()=}")
|
||||
self.setItem(row, TITLE, title_item)
|
||||
|
||||
artist_item = QTableWidgetItem(plr.track.artist)
|
||||
artist_item = QTableWidgetItem(row_data.track.artist)
|
||||
self.setItem(row, ARTIST, artist_item)
|
||||
|
||||
duration_item = QTableWidgetItem(
|
||||
ms_to_mmss(plr.track.duration))
|
||||
ms_to_mmss(row_data.track.duration))
|
||||
self.setItem(row, DURATION, duration_item)
|
||||
self._set_row_duration(row, plr.track.duration)
|
||||
self._set_row_duration(row, row_data.track.duration)
|
||||
|
||||
start_item = QTableWidgetItem()
|
||||
self.setItem(row, START_TIME, start_item)
|
||||
@ -632,8 +648,8 @@ class PlaylistTab(QTableWidget):
|
||||
end_item = QTableWidgetItem()
|
||||
self.setItem(row, END_TIME, end_item)
|
||||
|
||||
if plr.track.bitrate:
|
||||
bitrate = str(plr.track.bitrate)
|
||||
if row_data.track.bitrate:
|
||||
bitrate = str(row_data.track.bitrate)
|
||||
else:
|
||||
bitrate = ""
|
||||
bitrate_item = QTableWidgetItem(bitrate)
|
||||
@ -641,23 +657,23 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
# As we have track info, any notes should be contained in
|
||||
# the notes column
|
||||
notes_item = QTableWidgetItem(plr.note)
|
||||
notes_item = QTableWidgetItem(row_data.note)
|
||||
self.setItem(row, ROW_NOTES, notes_item)
|
||||
|
||||
last_playtime = Playdates.last_played(session, plr.track.id)
|
||||
last_playtime = Playdates.last_played(session, row_data.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(plr.track.path):
|
||||
if not file_is_readable(row_data.track.path):
|
||||
self._set_unreadable_row(row)
|
||||
|
||||
else:
|
||||
# This is a section header so it must have note text
|
||||
if plr.note is None:
|
||||
if row_data.note is None:
|
||||
log.debug(
|
||||
f"insert_row({plr=}) with no track_id and no note"
|
||||
f"insert_row({row_data=}) with no track_id and no note"
|
||||
)
|
||||
return
|
||||
|
||||
@ -671,16 +687,17 @@ class PlaylistTab(QTableWidget):
|
||||
continue
|
||||
self.setItem(row, i, QTableWidgetItem())
|
||||
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
|
||||
notes_item = QTableWidgetItem(plr.note)
|
||||
notes_item = QTableWidgetItem(row_data.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: Tracks,
|
||||
def insert_track(self, session: Session, track: Optional[Tracks],
|
||||
note: str = None, repaint: bool = True) -> None:
|
||||
"""
|
||||
Insert track into playlist tab.
|
||||
@ -692,30 +709,20 @@ class PlaylistTab(QTableWidget):
|
||||
to do the heavy lifing.
|
||||
"""
|
||||
|
||||
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))
|
||||
# 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)
|
||||
|
||||
def play_started(self, session: Session) -> None:
|
||||
"""
|
||||
@ -764,10 +771,10 @@ class PlaylistTab(QTableWidget):
|
||||
self._clear_current_track_row()
|
||||
self.current_track_start_time = None
|
||||
|
||||
def populate_display(self, session: Session, playlist_id: int,
|
||||
def populate(self, session: Session, playlist_id: int,
|
||||
scroll_to_top: bool = True) -> None:
|
||||
"""
|
||||
Populate display from the associated playlist ID
|
||||
Populate from the associated playlist ID
|
||||
"""
|
||||
|
||||
# Sanity check row numbering before we load
|
||||
@ -778,8 +785,8 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
# Add the rows
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
for plr in playlist.rows:
|
||||
self.insert_row(session, plr, repaint=False)
|
||||
for row in playlist.rows:
|
||||
self.insert_row(session, row, repaint=False)
|
||||
|
||||
# Scroll to top
|
||||
if scroll_to_top:
|
||||
@ -815,39 +822,22 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
def save_playlist(self, session: Session) -> None:
|
||||
"""
|
||||
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.
|
||||
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).
|
||||
"""
|
||||
|
||||
# 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())}
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
session.commit()
|
||||
PlaylistRows.delete_plrids_not_in_list(session, self.playlist_id,
|
||||
display_plr_ids.values())
|
||||
PlaylistRows.delete_higher_rows(session, self.playlist_id, row)
|
||||
|
||||
def scroll_current_to_top(self) -> None:
|
||||
"""Scroll currently-playing row to top"""
|
||||
@ -870,6 +860,65 @@ 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.
|
||||
@ -960,6 +1009,12 @@ 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"""
|
||||
|
||||
@ -999,6 +1054,7 @@ 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
|
||||
@ -1288,8 +1344,9 @@ 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
|
||||
@ -1302,10 +1359,13 @@ class PlaylistTab(QTableWidget):
|
||||
f"Really delete {row_count} row{plural}?"):
|
||||
return
|
||||
|
||||
self.remove_selected_rows()
|
||||
|
||||
with Session() as session:
|
||||
QTimer.singleShot(0, lambda: self.save_playlist(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()
|
||||
|
||||
def _drop_on(self, event):
|
||||
"""
|
||||
@ -1319,6 +1379,16 @@ 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]:
|
||||
"""
|
||||
@ -1470,6 +1540,16 @@ 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"""
|
||||
|
||||
@ -1578,20 +1658,6 @@ 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"""
|
||||
|
||||
@ -1703,65 +1769,6 @@ 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.
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
# 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
|
||||
@ -20,6 +21,7 @@ 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
|
||||
|
||||
@ -33,6 +35,15 @@ 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] = []
|
||||
@ -126,6 +137,10 @@ 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
|
||||
|
||||
@ -16,6 +16,20 @@ 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.
|
||||
@ -30,7 +44,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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user