Compare commits

...

7 Commits

Author SHA1 Message Date
Keith Edmunds
f182f49f15 More typing 2023-02-01 08:43:53 +00:00
Keith Edmunds
5d50ebf3aa Typing and other cleanups 2023-01-31 21:14:02 +00:00
Keith Edmunds
73bb4b3a7f WIP: typing 2023-01-30 19:29:33 +00:00
Keith Edmunds
dfb9326d5e Fix display corruption adding a track
Fixes #137
2023-01-29 18:31:50 +00:00
Keith Edmunds
e736cb82d2 Close MySQL session after running standalone commands 2023-01-27 10:53:28 +00:00
Keith Edmunds
d471082e3f Clean up dependencies, update them, add vulture settings 2023-01-20 22:01:52 +00:00
Keith Edmunds
e77c05b908 Remove unused functions 2023-01-20 22:01:05 +00:00
12 changed files with 120 additions and 276 deletions

View File

@ -5,21 +5,17 @@ import smtplib
import ssl import ssl
import tempfile import tempfile
from email.message import EmailMessage
from mutagen.flac import FLAC # type: ignore
from mutagen.mp3 import MP3 # type: ignore
from pydub import effects
from pydub.utils import mediainfo
from config import Config from config import Config
from datetime import datetime from datetime import datetime
from email.message import EmailMessage
from log import log from log import log
from pydub import AudioSegment from mutagen.flac import FLAC # type: ignore
from mutagen.mp3 import MP3 # type: ignore
from pydub import AudioSegment, effects
from pydub.utils import mediainfo
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from tinytag import TinyTag # type: ignore from tinytag import TinyTag # type: ignore
from typing import Optional from typing import Any, Dict, Optional, Union
# from typing import Dict, Optional, Union
from typing import Dict, Union
def ask_yes_no(title: str, question: str) -> bool: def ask_yes_no(title: str, question: str) -> bool:
@ -82,7 +78,7 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]:
return None return None
def get_tags(path: str) -> Dict[str, Union[str, int]]: def get_tags(path: str) -> Dict[str, Any]:
""" """
Return a dictionary of title, artist, duration-in-milliseconds and path. Return a dictionary of title, artist, duration-in-milliseconds and path.
""" """
@ -98,7 +94,7 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]:
) )
def get_relative_date(past_date: datetime, def get_relative_date(past_date: Optional[datetime],
reference_date: Optional[datetime] = None) -> str: reference_date: Optional[datetime] = None) -> str:
""" """
Return how long before reference_date past_date is as string. Return how long before reference_date past_date is as string.
@ -189,7 +185,8 @@ def send_mail(to_addr, from_addr, subj, body):
s.quit() s.quit()
def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str: def ms_to_mmss(ms: Optional[int], decimals: int = 0,
negative: bool = False) -> str:
"""Convert milliseconds to mm:ss""" """Convert milliseconds to mm:ss"""
minutes: int minutes: int

View File

@ -63,7 +63,7 @@ class Carts(Base):
def __init__(self, session: scoped_session, cart_number: int, def __init__(self, session: scoped_session, cart_number: int,
name: Optional[str] = None, name: Optional[str] = None,
duration: int = None, path: str = None, duration: Optional[int] = None, path: Optional[str] = None,
enabled: bool = True) -> None: enabled: bool = True) -> None:
"""Create new cart""" """Create new cart"""
@ -132,7 +132,7 @@ class Playdates(Base):
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
lastplayed = Column(DateTime, index=True, default=None) lastplayed = Column(DateTime, index=True, default=None)
track_id = Column(Integer, ForeignKey('tracks.id')) track_id = Column(Integer, ForeignKey('tracks.id'))
track = relationship("Tracks", back_populates="playdates") track: "Tracks" = relationship("Tracks", back_populates="playdates")
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -149,7 +149,8 @@ class Playdates(Base):
session.commit() session.commit()
@staticmethod @staticmethod
def last_played(session: scoped_session, track_id: int) -> Optional[datetime]: def last_played(session: scoped_session,
track_id: int) -> Optional[datetime]:
"""Return datetime track last played or None""" """Return datetime track last played or None"""
last_played = session.execute( last_played = session.execute(
@ -165,7 +166,8 @@ class Playdates(Base):
return None return None
@staticmethod @staticmethod
def played_after(session: scoped_session, since: datetime) -> List["Playdates"]: def played_after(session: scoped_session,
since: datetime) -> List["Playdates"]:
"""Return a list of Playdates objects since passed time""" """Return a list of Playdates objects since passed time"""
return ( return (
@ -186,7 +188,7 @@ class Playlists(Base):
__tablename__ = "playlists" __tablename__ = "playlists"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
name = Column(String(32), nullable=False, unique=True) name = Column(String(32), nullable=False, unique=True)
last_used = Column(DateTime, default=None, nullable=True) last_used = Column(DateTime, default=None, nullable=True)
tab = Column(Integer, default=None, nullable=True, unique=True) tab = Column(Integer, default=None, nullable=True, unique=True)
@ -194,7 +196,7 @@ class Playlists(Base):
is_template = Column(Boolean, default=False, nullable=False) is_template = Column(Boolean, default=False, nullable=False)
query = Column(String(256), default=None, nullable=True, unique=False) query = Column(String(256), default=None, nullable=True, unique=False)
deleted = Column(Boolean, default=False, nullable=False) deleted = Column(Boolean, default=False, nullable=False)
rows = relationship( rows: "PlaylistRows" = relationship(
"PlaylistRows", "PlaylistRows",
back_populates="playlist", back_populates="playlist",
cascade="all, delete-orphan", cascade="all, delete-orphan",
@ -207,10 +209,10 @@ class Playlists(Base):
f"is_templatee={self.is_template}>" f"is_templatee={self.is_template}>"
) )
def __init__(self, session: scoped_session, name: str) -> None: def __init__(self, session: scoped_session, name: str):
self.name = name self.name = name
session.add(self) session.add(self)
session.commit() session.flush()
def close(self, session: scoped_session) -> None: def close(self, session: scoped_session) -> None:
"""Mark playlist as unloaded""" """Mark playlist as unloaded"""
@ -231,10 +233,15 @@ class Playlists(Base):
session: scoped_session, session: scoped_session,
template: "Playlists", template: "Playlists",
playlist_name: str) \ playlist_name: str) \
-> "Playlists": -> Optional["Playlists"]:
"""Create a new playlist from template""" """Create a new playlist from template"""
playlist = cls(session, playlist_name) playlist = cls(session, playlist_name)
# Sanity / mypy checks
if not playlist or not playlist.id or not template.id:
return None
PlaylistRows.copy_playlist(session, template.id, playlist.id) PlaylistRows.copy_playlist(session, template.id, playlist.id)
return playlist return playlist
@ -332,6 +339,9 @@ class Playlists(Base):
"""Save passed playlist as new template""" """Save passed playlist as new template"""
template = Playlists(session, template_name) template = Playlists(session, template_name)
if not template or not template.id:
return
template.is_template = True template.is_template = True
session.commit() session.commit()
@ -345,9 +355,9 @@ class PlaylistRows(Base):
row_number = Column(Integer, nullable=False) row_number = Column(Integer, nullable=False)
note = Column(String(2048), index=False) note = Column(String(2048), index=False)
playlist_id = Column(Integer, ForeignKey('playlists.id'), nullable=False) playlist_id = Column(Integer, ForeignKey('playlists.id'), nullable=False)
playlist = relationship(Playlists, back_populates="rows") playlist: Playlists = relationship(Playlists, back_populates="rows")
track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True) track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True)
track = relationship("Tracks", back_populates="playlistrows") track: "Tracks" = relationship("Tracks", back_populates="playlistrows")
played = Column(Boolean, nullable=False, index=False, default=False) played = Column(Boolean, nullable=False, index=False, default=False)
def __repr__(self) -> str: def __repr__(self) -> str:
@ -448,7 +458,8 @@ class PlaylistRows(Base):
).first() ).first()
@staticmethod @staticmethod
def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]: def get_last_used_row(session: scoped_session,
playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows""" """Return the last used row for playlist, or None if no rows"""
return session.execute( return session.execute(
@ -515,8 +526,8 @@ class PlaylistRows(Base):
return plrs return plrs
@staticmethod @staticmethod
def move_rows_down(session: scoped_session, playlist_id: int, starting_row: int, def move_rows_down(session: scoped_session, playlist_id: int,
move_by: int) -> None: starting_row: int, move_by: int) -> None:
""" """
Create space to insert move_by additional rows by incremented row Create space to insert move_by additional rows by incremented row
number from starting_row to end of playlist number from starting_row to end of playlist
@ -567,27 +578,27 @@ class Settings(Base):
value = self.f_datetime or self.f_int or self.f_string value = self.f_datetime or self.f_int or self.f_string
return f"<Settings(id={self.id}, name={self.name}, {value=}>" return f"<Settings(id={self.id}, name={self.name}, {value=}>"
def __init__(self, session: scoped_session, name: str):
self.name = name
session.add(self)
session.flush()
@classmethod @classmethod
def get_int_settings(cls, session: scoped_session, name: str) -> "Settings": def get_int_settings(cls, session: scoped_session,
name: str) -> "Settings":
"""Get setting for an integer or return new setting record""" """Get setting for an integer or return new setting record"""
int_setting: Settings
try: try:
int_setting = session.execute( return session.execute(
select(cls) select(cls)
.where(cls.name == name) .where(cls.name == name)
).scalar_one() ).scalar_one()
except NoResultFound: except NoResultFound:
int_setting = Settings() return Settings(session, name)
int_setting.name = name
int_setting.f_int = None
session.add(int_setting)
return int_setting def update(self, session: scoped_session, data: dict):
def update(self, session: scoped_session, data: "Settings"):
for key, value in data.items(): for key, value in data.items():
assert hasattr(self, key) assert hasattr(self, key)
setattr(self, key, value) setattr(self, key, value)
@ -607,9 +618,10 @@ class Tracks(Base):
path = Column(String(2048), index=False, nullable=False, unique=True) path = Column(String(2048), index=False, nullable=False, unique=True)
mtime = Column(Float, index=True) mtime = Column(Float, index=True)
bitrate = Column(Integer, nullable=True, default=None) bitrate = Column(Integer, nullable=True, default=None)
playlistrows = relationship("PlaylistRows", back_populates="track") playlistrows: PlaylistRows = relationship("PlaylistRows",
back_populates="track")
playlists = association_proxy("playlistrows", "playlist") playlists = association_proxy("playlistrows", "playlist")
playdates = relationship("Playdates", back_populates="track") playdates: Playdates = relationship("Playdates", back_populates="track")
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -650,7 +662,8 @@ class Tracks(Base):
return session.execute(select(cls)).scalars().all() return session.execute(select(cls)).scalars().all()
@classmethod @classmethod
def get_by_path(cls, session: scoped_session, path: str) -> "Tracks": def get_by_path(cls, session: scoped_session,
path: str) -> Optional["Tracks"]:
""" """
Return track with passed path, or None. Return track with passed path, or None.
""" """
@ -666,7 +679,8 @@ class Tracks(Base):
return None return None
@classmethod @classmethod
def search_artists(cls, session: scoped_session, text: str) -> List["Tracks"]: def search_artists(cls, session: scoped_session,
text: str) -> List["Tracks"]:
"""Search case-insenstively for artists containing str""" """Search case-insenstively for artists containing str"""
return ( return (
@ -680,7 +694,8 @@ class Tracks(Base):
) )
@classmethod @classmethod
def search_titles(cls, session: scoped_session, text: str) -> List["Tracks"]: def search_titles(cls, session: scoped_session,
text: str) -> List["Tracks"]:
"""Search case-insenstively for titles containing str""" """Search case-insenstively for titles containing str"""
return ( return (
session.execute( session.execute(

View File

@ -27,7 +27,7 @@ from PyQt5.QtWidgets import (
QProgressBar, QProgressBar,
) )
from dbconfig import engine, Session from dbconfig import engine, Session, scoped_session
import helpers import helpers
import music import music
@ -59,7 +59,8 @@ class CartButton(QPushButton):
"""Create a cart pushbutton and set it disabled""" """Create a cart pushbutton and set it disabled"""
super().__init__(parent) super().__init__(parent)
self.parent = parent # Next line is redundant (check)
# self.parent = parent
self.cart_id = cart.id self.cart_id = cart.id
if cart.path and cart.enabled and not cart.duration: if cart.path and cart.enabled and not cart.duration:
tags = helpers.get_tags(cart.path) tags = helpers.get_tags(cart.path)
@ -126,28 +127,28 @@ class PlaylistTrack:
number: that's the playlist's problem. number: that's the playlist's problem.
""" """
self.artist = None self.artist: Optional[str] = None
self.duration = None self.duration: Optional[int] = None
self.end_time = None self.end_time: Optional[datetime] = None
self.fade_at = None self.fade_at: Optional[int] = None
self.fade_length = None self.fade_length: Optional[int] = None
self.path = None self.path: Optional[str] = None
self.playlist_id = None self.playlist_id: Optional[int] = None
self.playlist_tab = None self.playlist_tab: Optional[PlaylistTab] = None
self.plr_id = None self.plr_id: Optional[int] = None
self.silence_at = None self.silence_at: Optional[datetime] = None
self.start_gap = None self.start_gap: Optional[int] = None
self.start_time = None self.start_time: Optional[datetime] = None
self.title = None self.title: Optional[str] = None
self.track_id = None self.track_id: Optional[int] = None
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, " f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"row_number={self.row_number} playlist_id={self.playlist_id}>" f"playlist_id={self.playlist_id}>"
) )
def set_plr(self, session: Session, plr: PlaylistRows, def set_plr(self, session: scoped_session, plr: PlaylistRows,
tab: PlaylistTab) -> None: tab: PlaylistTab) -> None:
""" """
Update with new plr information Update with new plr information
@ -492,7 +493,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
def create_playlist(self, def create_playlist(self,
session: Session, session: scoped_session,
playlist_name: Optional[str] = None) -> Playlists: playlist_name: Optional[str] = None) -> Playlists:
"""Create new playlist""" """Create new playlist"""
@ -512,7 +513,7 @@ class Window(QMainWindow, Ui_MainWindow):
if playlist: if playlist:
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session: Session, def create_playlist_tab(self, session: scoped_session,
playlist: Playlists) -> int: playlist: Playlists) -> int:
""" """
Take the passed playlist database object, create a playlist tab and Take the passed playlist database object, create a playlist tab and
@ -714,7 +715,7 @@ class Window(QMainWindow, Ui_MainWindow):
QMessageBox.Ok QMessageBox.Ok
) )
def get_one_track(self, session: Session) -> Optional[Tracks]: def get_one_track(self, session: scoped_session) -> Optional[Tracks]:
"""Show dialog box to select one track and return it to caller""" """Show dialog box to select one track and return it to caller"""
dlg = DbDialog(self, session, get_one_track=True) dlg = DbDialog(self, session, get_one_track=True)
@ -833,10 +834,10 @@ class Window(QMainWindow, Ui_MainWindow):
_ = self.create_playlist_tab(session, playlist) _ = self.create_playlist_tab(session, playlist)
# Set active tab # Set active tab
record = Settings.get_int_settings(session, "active_tab") record = Settings.get_int_settings(session, "active_tab")
if record and record.f_int is not None: if record and record.f_int >= 0:
self.tabPlaylist.setCurrentIndex(record.f_int) self.tabPlaylist.setCurrentIndex(record.f_int)
def move_playlist_rows(self, session: Session, def move_playlist_rows(self, session: scoped_session,
playlistrows: List[PlaylistRows]) -> None: playlistrows: List[PlaylistRows]) -> None:
""" """
Move passed playlist rows to another playlist Move passed playlist rows to another playlist
@ -1325,7 +1326,7 @@ class Window(QMainWindow, Ui_MainWindow):
# May also be called when last tab is closed # May also be called when last tab is closed
pass pass
def this_is_the_next_playlist_row(self, session: Session, def this_is_the_next_playlist_row(self, session: scoped_session,
plr: PlaylistRows, plr: PlaylistRows,
playlist_tab: PlaylistTab) -> None: playlist_tab: PlaylistTab) -> None:
""" """
@ -1468,6 +1469,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Time to end # Time to end
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
# Autoplay next track
# if time_to_silence <= 1500:
# self.play_next()
else: else:
if self.playing: if self.playing:
self.stop_playing() self.stop_playing()
@ -1500,7 +1504,7 @@ class Window(QMainWindow, Ui_MainWindow):
class CartDialog(QDialog): class CartDialog(QDialog):
"""Edit cart details""" """Edit cart details"""
def __init__(self, parent: QMainWindow, session: Session, def __init__(self, parent: QMainWindow, session: scoped_session,
cart: Carts) -> None: cart: Carts) -> None:
""" """
Manage carts Manage carts
@ -1537,7 +1541,7 @@ class CartDialog(QDialog):
class DbDialog(QDialog): class DbDialog(QDialog):
"""Select track from database""" """Select track from database"""
def __init__(self, parent: QMainWindow, session: Session, def __init__(self, parent: QMainWindow, session: scoped_session,
get_one_track: bool = False) -> None: get_one_track: bool = False) -> None:
""" """
Subclassed QDialog to manage track selection Subclassed QDialog to manage track selection
@ -1762,10 +1766,12 @@ if __name__ == "__main__":
log.debug("Updating database") log.debug("Updating database")
with Session() as session: with Session() as session:
check_db(session) check_db(session)
engine.dispose()
elif args.update_bitrates: elif args.update_bitrates:
log.debug("Update bitrates") log.debug("Update bitrates")
with Session() as session: with Session() as session:
update_bitrates(session) update_bitrates(session)
engine.dispose()
else: else:
# Normal run # Normal run
try: try:
@ -1773,7 +1779,9 @@ if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
win = Window() win = Window()
win.show() win.show()
sys.exit(app.exec()) status = app.exec()
engine.dispose()
sys.exit(status)
except Exception as exc: except Exception as exc:
from helpers import send_mail from helpers import send_mail

View File

@ -435,7 +435,6 @@ class PlaylistTab(QTableWidget):
track = session.get(Tracks, track_id) track = session.get(Tracks, track_id)
if track: if track:
if self.edit_cell_type == TITLE: if self.edit_cell_type == TITLE:
log.debug(f"KAE: _cell_changed:440, {new_text=}")
track.title = new_text track.title = new_text
if update_current: if update_current:
self.musicmuster.current_track.title = new_text self.musicmuster.current_track.title = new_text
@ -608,7 +607,6 @@ class PlaylistTab(QTableWidget):
self.setItem(row, START_GAP, start_gap_item) self.setItem(row, START_GAP, start_gap_item)
title_item = QTableWidgetItem(plr.track.title) title_item = QTableWidgetItem(plr.track.title)
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(plr.track.artist) artist_item = QTableWidgetItem(plr.track.artist)
@ -667,7 +665,8 @@ class PlaylistTab(QTableWidget):
userdata_item.setData(self.ROW_TRACK_ID, 0) userdata_item.setData(self.ROW_TRACK_ID, 0)
if repaint: if repaint:
self.update_display(session) # Schedule so that display can update with new row first
QTimer.singleShot(0, lambda: self.update_display(session))
def insert_track(self, session: scoped_session, track: Tracks, def insert_track(self, session: scoped_session, track: Tracks,
note: Optional[str] = None, repaint: bool = True) -> None: note: Optional[str] = None, repaint: bool = True) -> None:
@ -1206,7 +1205,7 @@ class PlaylistTab(QTableWidget):
return start + timedelta(milliseconds=duration) return start + timedelta(milliseconds=duration)
def _column_resize(self, idx: int, old: int, new: int) -> None: def _column_resize(self, idx: int, _old: int, _new: int) -> None:
""" """
Called when column widths are changed. Called when column widths are changed.
@ -1361,7 +1360,7 @@ class PlaylistTab(QTableWidget):
return playlistrow_id return playlistrow_id
def _get_playlistrow_object(self, session: scoped_session, def _get_playlistrow_object(self, session: scoped_session,
row: int) -> int: row: int) -> PlaylistRows:
"""Return the playlistrow object associated with this row""" """Return the playlistrow object associated with this row"""
playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID)) playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID))
@ -1490,48 +1489,6 @@ class PlaylistTab(QTableWidget):
and pos.y() >= rect.center().y() # noqa W503 and pos.y() >= rect.center().y() # noqa W503
) )
def _meta_clear_attribute(self, row: int, attribute: int) -> None:
"""Clear given metadata for row"""
if row is None:
raise ValueError(f"_meta_clear_attribute({row=}, {attribute=})")
new_metadata: int = self._meta_get(row) & ~(1 << attribute)
self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata)
def _meta_get(self, row: int) -> int:
"""Return row metadata"""
return (self.item(row, USERDATA).data(self.ROW_FLAGS))
def _meta_search(self, metadata: int, one: bool = True) -> List[int]:
"""
Search rows for metadata.
If one is True, check that only one row matches and return
the row number.
If one is False, return a list of matching row numbers.
"""
matches = []
for row in range(self.rowCount()):
if self._meta_get(row):
if self._meta_get(row) & (1 << metadata):
matches.append(row)
if not one:
return matches
if len(matches) <= 1:
return matches
else:
log.error(
f"Multiple matches for metadata '{metadata}' found "
f"in rows: {', '.join([str(x) for x in matches])}"
)
raise AttributeError(f"Multiple '{metadata}' metadata {matches}")
def _move_row(self, session: scoped_session, plr: PlaylistRows, def _move_row(self, session: scoped_session, plr: PlaylistRows,
new_row_number: int) -> None: new_row_number: int) -> None:
"""Move playlist row to new_row_number using parent copy/paste""" """Move playlist row to new_row_number using parent copy/paste"""
@ -1765,7 +1722,7 @@ class PlaylistTab(QTableWidget):
continue continue
attr_name = f"playlist_{column_name}_col_width" attr_name = f"playlist_{column_name}_col_width"
record: Settings = Settings.get_int_settings(session, attr_name) record: Settings = Settings.get_int_settings(session, attr_name)
if record and record.f_int is not None: if record and record.f_int >= 0:
self.setColumnWidth(idx, record.f_int) self.setColumnWidth(idx, record.f_int)
else: else:
self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH)
@ -1933,7 +1890,6 @@ class PlaylistTab(QTableWidget):
item_startgap.setBackground(QColor("white")) item_startgap.setBackground(QColor("white"))
item_title = self.item(row, TITLE) item_title = self.item(row, TITLE)
log.debug(f"KAE: _update_row:1978, {track.title=}")
item_title.setText(track.title) item_title.setText(track.title)
item_artist = self.item(row, ARTIST) item_artist = self.item(row, ARTIST)

View File

@ -35,9 +35,9 @@ parent_dir = os.path.dirname(source_dir)
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] = []
no_match: List[str] = [] no_match: List[str] = []
possibles: List[str] = [] # possibles: List[str] = []
no_match: int = 0 no_match: int = 0

View File

@ -1,17 +0,0 @@
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontMetrics, QPainter
from PyQt5.QtWidgets import QLabel
# class ElideLabel(QLabel):
# """
# From https://stackoverflow.com/questions/11446478/
# pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize
# """
#
# def paintEvent(self, event):
# painter = QPainter(self)
# metrics = QFontMetrics(self.font())
# elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
#
# painter.drawText(self.rect(), self.alignment(), elided)

View File

@ -70,11 +70,4 @@ def do_command(command):
return response return response
def quick_test():
"""Example list of commands."""
do_command('Help: Command=Help')
do_command('Help: Command="GetInfo"')
# do_command('SetPreference: Name=GUI/Theme Value=classic Reload=1')
do_command('Import2: Filename=/home/kae/git/musicmuster/archive/boot.flac') do_command('Import2: Filename=/home/kae/git/musicmuster/archive/boot.flac')

94
play.py
View File

@ -1,94 +0,0 @@
#!/usr/bin/env python
from sqlalchemy import create_engine
from sqlalchemy import text
from sqlalchemy import Table, Column, Integer, String
from sqlalchemy import ForeignKey
from sqlalchemy import select
from sqlalchemy import insert
from sqlalchemy.orm import Session
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True)
class User(Base):
__tablename__ = 'user_account'
id = Column(Integer, primary_key=True)
name = Column(String(30))
fullname = Column(String)
addresses = relationship("Address", back_populates="user")
def __repr__(self):
return (
f"User(id={self.id!r}, name={self.name!r}, "
f"fullname={self.fullname!r})"
)
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email_address = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey('user_account.id'))
user = relationship("User", back_populates="addresses")
def __repr__(self):
return f"Address(id={self.id!r}, email_address={self.email_address!r})"
Base.metadata.create_all(engine)
squidward = User(name="squidward", fullname="Squidward Tentacles")
krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")
session = Session(engine)
session.add(squidward)
session.add(krabs)
session.commit()
u1 = User(name='pkrabs', fullname='Pearl Krabs')
a1 = Address(email_address="pearl.krabs@gmail.com")
u1.addresses.append(a1)
a2 = Address(email_address="pearl@aol.com", user=u1)
session.add(u1)
session.add(a1)
session.add(a2)
session.commit()
# with engine.connect() as conn:
# conn.execute(text("CREATE TABLE some_table (x int, y int)"))
# conn.execute(
# text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
# [{"x": 1, "y": 1}, {"x": 2, "y": 4}]
# )
# conn.commit()
#
# with engine.begin() as conn:
# conn.execute(
# text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
# [{"x": 6, "y": 8}, {"x": 9, "y": 10}]
# )
#
# # with engine.connect() as conn:
# # result = conn.execute(text("SELECT x, y FROM some_table"))
# # for row in result:
# # print(f"x: {row.x} y: {row.y}")
#
#
# stmt = text(
# "SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y").bindparams(y=6)
#
# with Session(engine) as session:
# result = session.execute(stmt)
# for row in result:
# print(f"x: {row.x} y: {row.y}")

30
poetry.lock generated
View File

@ -306,7 +306,7 @@ python-versions = ">=3.7"
name = "mypy" name = "mypy"
version = "0.991" version = "0.991"
description = "Optional static typing for Python" description = "Optional static typing for Python"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -325,7 +325,7 @@ reports = ["lxml"]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker." description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "main" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
@ -671,23 +671,11 @@ postgresql_psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql (<1)", "pymysql"] pymysql = ["pymysql (<1)", "pymysql"]
sqlcipher = ["sqlcipher3-binary"] sqlcipher = ["sqlcipher3-binary"]
[[package]]
name = "sqlalchemy-stubs"
version = "0.4"
description = "SQLAlchemy stubs and mypy plugin"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
mypy = ">=0.790"
typing-extensions = ">=3.7.4"
[[package]] [[package]]
name = "sqlalchemy2-stubs" name = "sqlalchemy2-stubs"
version = "0.0.2a31" version = "0.0.2a32"
description = "Typing Stubs for SQLAlchemy 1.4" description = "Typing Stubs for SQLAlchemy 1.4"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -760,7 +748,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -788,7 +776,7 @@ python-versions = "*"
name = "typing-extensions" name = "typing-extensions"
version = "4.4.0" version = "4.4.0"
description = "Backported and Experimental Type Hints for Python 3.7+" description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -816,7 +804,7 @@ python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "9ae8194c057d5eb51e7f65f5328f94ea85e0a48a5fe12d38ad72f4a1aae7cbca" content-hash = "389d73715056202a39f0efe23941943d7a6849915607cb8ceb4c77bde7e4f709"
[metadata.files] [metadata.files]
alembic = [] alembic = []
@ -922,10 +910,6 @@ six = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
] ]
sqlalchemy = [] sqlalchemy = []
sqlalchemy-stubs = [
{file = "sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae"},
{file = "sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5"},
]
sqlalchemy2-stubs = [] sqlalchemy2-stubs = []
stack-data = [] stack-data = []
stackprinter = [] stackprinter = []

View File

@ -24,18 +24,17 @@ python-Levenshtein = "^0.12.2"
pyfzf = "^0.3.1" pyfzf = "^0.3.1"
pydymenu = "^0.5.2" pydymenu = "^0.5.2"
stackprinter = "^0.2.10" stackprinter = "^0.2.10"
sqlalchemy-stubs = "^0.4"
sqlalchemy2-stubs = "^0.0.2-alpha.31"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
ipdb = "^0.13.9" ipdb = "^0.13.9"
sqlalchemy-stubs = "^0.4"
PyQt5-stubs = "^5.15.2" PyQt5-stubs = "^5.15.2"
pytest = "^7.0.1" pytest = "^7.0.1"
pytest-qt = "^4.0.2" pytest-qt = "^4.0.2"
pydub-stubs = "^0.25.1" pydub-stubs = "^0.25.1"
line-profiler = "^4.0.2" line-profiler = "^4.0.2"
flakehell = "^0.9.0" flakehell = "^0.9.0"
sqlalchemy2-stubs = "^0.0.2-alpha.32"
mypy = "^0.991"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
@ -45,3 +44,7 @@ build-backend = "poetry.core.masonry.api"
mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app" mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app"
plugins = "sqlalchemy.ext.mypy.plugin" plugins = "sqlalchemy.ext.mypy.plugin"
[tool.vulture]
exclude = ["migrations", "app/ui", "archive"]
paths = ["app"]
make_whitelist = true

16
test.py
View File

@ -2,28 +2,28 @@
from PyQt5 import QtGui, QtWidgets from PyQt5 import QtGui, QtWidgets
class TabBar(QtWidgets.QTabWidget): class TabBar(QtWidgets.QTabBar):
def paintEvent(self, event): def paintEvent(self, event):
painter = QtGui.QStylePainter(self) painter = QtWidgets.QStylePainter(self)
option = QtGui.QStyleOptionTab() option = QtWidgets.QStyleOptionTab()
for index in range(self.count()): for index in range(self.count()):
self.initStyleOption(option, index) self.initStyleOption(option, index)
bgcolor = QtGui.QColor(self.tabText(index)) bgcolor = QtGui.QColor(self.tabText(index))
option.palette.setColor(QtGui.QPalette.Window, bgcolor) option.palette.setColor(QtGui.QPalette.Window, bgcolor)
painter.drawControl(QtGui.QStyle.CE_TabBarTabShape, option) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
painter.drawControl(QtGui.QStyle.CE_TabBarTabLabel, option) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option)
class Window(QtWidgets.QTabWidget): class Window(QtWidgets.QTabWidget):
def __init__(self): def __init__(self):
QtGui.QTabWidget.__init__(self) QtWidgets.QTabWidget.__init__(self)
self.setTabBar(TabBar(self)) self.setTabBar(TabBar(self))
for color in 'tomato orange yellow lightgreen skyblue plum'.split(): for color in 'tomato orange yellow lightgreen skyblue plum'.split():
self.addTab(QtGui.QWidget(self), color) self.addTab(QtWidgets.QWidget(self), color)
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
app = QtGui.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
window = Window() window = Window()
window.resize(420, 200) window.resize(420, 200)
window.show() window.show()

View File

@ -5,7 +5,6 @@ from app.models import (
Notes, Notes,
Playdates, Playdates,
Playlists, Playlists,
PlaylistTracks,
Tracks, Tracks,
) )
@ -205,11 +204,11 @@ def test_playlist_notes(session):
# We need two notes # We need two notes
note1_text = "note1 text" note1_text = "note1 text"
note1_row = 11 note1_row = 11
note1 = Notes(session, playlist.id, note1_row, note1_text) _ = Notes(session, playlist.id, note1_row, note1_text)
note2_text = "note2 text" note2_text = "note2 text"
note2_row = 19 note2_row = 19
note2 = Notes(session, playlist.id, note2_row, note2_text) _ = Notes(session, playlist.id, note2_row, note2_text)
notes = playlist.notes notes = playlist.notes
assert note1_text in [n.note for n in notes] assert note1_text in [n.note for n in notes]