This commit is contained in:
Keith Edmunds 2022-03-14 20:10:59 +00:00
parent a31718d2b9
commit 87f9e1e81b
8 changed files with 192 additions and 158 deletions

View File

@ -1,12 +1,11 @@
#!/usr/bin/python3
import dbconfig
import os.path
import re
from dbconfig import Session
from datetime import datetime
from dbconfig import engine
from typing import List, Optional
from pydub import AudioSegment
@ -40,10 +39,8 @@ from helpers import (
)
from log import DEBUG, ERROR
Session = dbconfig.session
Base: DeclarativeMeta = declarative_base()
Base.metadata.create_all(engine)
# Database classes
@ -70,7 +67,7 @@ class NoteColours(Base):
self.order = order
session.add(self)
session.commit()
session.flush()
def __repr__(self) -> str:
return (
@ -142,7 +139,7 @@ class Notes(Base):
self.row = row
self.note = text
session.add(self)
session.commit()
session.flush()
def __repr__(self) -> str:
return (
@ -155,7 +152,7 @@ class Notes(Base):
DEBUG(f"delete_note({self.id=}")
session.query(Notes).filter_by(id=self.id).delete()
session.commit()
session.flush()
@classmethod
def get_by_id(cls, session: Session, note_id: int) -> Optional["Notes"]:
@ -181,7 +178,7 @@ class Notes(Base):
self.row = row
if text:
self.note = text
session.commit()
session.flush()
class Playdates(Base):
@ -202,7 +199,7 @@ class Playdates(Base):
self.track_id = track.id
track.update_lastplayed(session)
session.add(self)
session.commit()
session.flush()
@staticmethod
def last_played(session: Session, track_id: int) -> Optional[datetime]:
@ -224,9 +221,8 @@ class Playdates(Base):
"""
session.query(Playdates).filter(
Playdates.track_id == track_id,
).delete()
session.commit()
Playdates.track_id == track_id).delete()
session.flush()
class Playlists(Base):
@ -251,7 +247,7 @@ class Playlists(Base):
def __init__(self, session: Session, name: str) -> None:
self.name = name
session.add(self)
session.commit()
session.flush()
def __repr__(self) -> str:
return f"<Playlists(id={self.id}, name={self.name}>"
@ -279,7 +275,7 @@ class Playlists(Base):
self.loaded = False
session.add(self)
session.commit()
session.flush()
@classmethod
def get_all(cls, session: Session) -> List["Playlists"]:
@ -320,25 +316,38 @@ class Playlists(Base):
self.loaded = True
self.last_used = datetime.now()
session.commit()
session.flush()
def move_track(self, session: Session, rows: List[int],
to_playlist: "Playlists") -> None:
"""Move tracks to another playlist"""
for row in rows:
track = self.tracks[row]
to_playlist.add_track(session, track.id)
del self.tracks[row]
session.flush()
def remove_all_tracks(self, session: Session) -> None:
"""
Remove all tracks from this playlist
"""
session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == self.id,
).delete()
session.commit()
self.tracks = {}
session.flush()
def remove_track(self, session: Session, row: int) -> None:
DEBUG(f"Playlist.remove_track({self.id=}, {row=})")
session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == self.id,
PlaylistTracks.row == row
).delete()
# Get tracks collection for this playlist
tracks_collections = self.tracks
# Tracks are a dictionary of tracks keyed on row
# number. Remove the relevant row.
del tracks_collections[row]
# Save the new tracks collection
self.tracks = tracks_collections
session.flush()
class PlaylistTracks(Base):
@ -355,7 +364,8 @@ class PlaylistTracks(Base):
backref=backref(
"playlist_tracks",
collection_class=attribute_mapped_collection("row"),
lazy="joined"
lazy="joined",
cascade="all, delete-orphan"
)
)
@ -368,45 +378,7 @@ class PlaylistTracks(Base):
self.track_id = track_id
self.row = row
session.add(self)
session.commit()
@staticmethod
def move_track(
session: Session, from_playlist_id: int, row: int,
to_playlist_id: int) -> None:
"""
Move track between playlists. This would be more efficient with
an ORM-enabled UPDATE statement, but this works just fine.
"""
DEBUG(
"PlaylistTracks.move_tracks("
f"{from_playlist_id=}, {row=}, {to_playlist_id=})"
)
new_row: int
max_row: Optional[int] = session.query(
func.max(PlaylistTracks.row)).filter(
PlaylistTracks.playlist_id == to_playlist_id).scalar()
if max_row is None:
# Destination playlist is empty; use row 0
new_row = 0
else:
# Destination playlist has tracks; add to end
new_row = max_row + 1
try:
record: PlaylistTracks = session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == from_playlist_id,
PlaylistTracks.row == row).one()
except NoResultFound:
ERROR(
f"No rows matched in query: "
f"PlaylistTracks.playlist_id == {from_playlist_id}, "
f"PlaylistTracks.row == {row}"
)
return
record.playlist_id = to_playlist_id
record.row = new_row
session.commit()
session.flush()
@staticmethod
def next_free_row(session: Session, playlist: Playlists) -> int:
@ -449,14 +421,14 @@ class Settings(Base):
int_setting.name = name
int_setting.f_int = None
session.add(int_setting)
session.commit()
session.flush()
return int_setting
def update(self, session: Session, data):
for key, value in data.items():
assert hasattr(self, key)
setattr(self, key, value)
session.commit()
session.flush()
class Tracks(Base):
@ -479,10 +451,30 @@ class Tracks(Base):
back_populates="tracks",
lazy="joined")
def __init__(self, session: Session, path: str) -> None:
def __init__(self,
session: Session,
path: str,
title: Optional[str] = None,
artist: Optional[str] = None,
duration: Optional[int] = None,
start_gap: Optional[int] = None,
fade_at: Optional[int] = None,
silence_at: Optional[int] = None,
mtime: Optional[float] = None,
lastplayed: Optional[datetime] = None,
) -> None:
self.path = path
self.title = title
self.artist = artist
self.duration = duration
self.start_gap = start_gap
self.fade_at = fade_at
self.silence_at = silence_at
self.mtime = mtime
self.lastplayed = lastplayed
session.add(self)
session.commit()
session.flush()
def __repr__(self) -> str:
return (
@ -571,7 +563,7 @@ class Tracks(Base):
Config.MILLISECOND_SIGFIGS) * 1000
self.start_gap = leading_silence(audio)
session.add(self)
session.commit()
session.flush()
@staticmethod
def remove_by_path(session: Session, path: str) -> None:
@ -581,7 +573,7 @@ class Tracks(Base):
try:
session.query(Tracks).filter(Tracks.path == path).delete()
session.commit()
session.flush()
except IntegrityError as exception:
ERROR(f"Can't remove track with {path=} ({exception=})")
@ -605,17 +597,17 @@ class Tracks(Base):
def update_lastplayed(self, session: Session) -> None:
self.lastplayed = datetime.now()
session.add(self)
session.commit()
session.flush()
def update_artist(self, session: Session, artist: str) -> None:
self.artist = artist
session.add(self)
session.commit()
session.flush()
def update_title(self, session: Session, title: str) -> None:
self.title = title
session.add(self)
session.commit()
session.flush()
def update_path(self, newpath: str) -> None:
self.path = newpath

View File

@ -23,6 +23,7 @@ from PyQt5.QtWidgets import (
QMainWindow,
)
import dbconfig
import helpers
import music
@ -979,15 +980,13 @@ class SelectPlaylistDialog(QDialog):
self.accept()
def main():
if __name__ == "__main__":
try:
Base.metadata.create_all(dbconfig.engine)
Session = dbconfig.Session
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
except Exception:
EXCEPTION("Unhandled Exception caught by musicmuster.main()")
if __name__ == "__main__":
main()

View File

@ -27,11 +27,11 @@ from models import (
Notes,
Playdates,
Playlists,
Session,
Settings,
Tracks,
NoteColours
)
from dbconfig import Session
class RowMeta:
@ -101,7 +101,7 @@ class PlaylistTab(QTableWidget):
self.setHorizontalHeaderItem(7, item)
self.horizontalHeader().setMinimumSectionSize(0)
self._set_column_widths()
self._set_column_widths(session)
self.setHorizontalHeaderLabels([
Config.COLUMN_NAME_AUTOPLAY,
Config.COLUMN_NAME_LEADING_SILENCE,
@ -1368,7 +1368,7 @@ class PlaylistTab(QTableWidget):
ms: int = 0
with Session() as session:
for row in (sel_rows - notes_rows):
ms += self._get_row_track_object(row, session).duration
ms += self._get_row_track_object(row, session).duration or 0
# Only paint message if there are selected track rows
if ms > 0:
@ -1398,17 +1398,16 @@ class PlaylistTab(QTableWidget):
# Reset extended selection
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def _set_column_widths(self) -> None:
def _set_column_widths(self, session: Session) -> None:
"""Column widths from settings"""
with Session() as session:
for column in range(self.columnCount()):
name: str = f"playlist_col_{str(column)}_width"
record: Settings = Settings.get_int_settings(session, name)
if record and record.f_int is not None:
self.setColumnWidth(column, record.f_int)
else:
self.setColumnWidth(column, Config.DEFAULT_COLUMN_WIDTH)
for column in range(self.columnCount()):
name: str = f"playlist_col_{str(column)}_width"
record: Settings = Settings.get_int_settings(session, name)
if record and record.f_int is not None:
self.setColumnWidth(column, record.f_int)
else:
self.setColumnWidth(column, Config.DEFAULT_COLUMN_WIDTH)
def _set_next(self, row: int, session: Session) -> None:
"""

View File

@ -53,7 +53,7 @@ def main():
DEBUG("Finished")
def create_track_from_file(session, path, interactive=False):
def create_track_from_file(session, path, normalise=None, interactive=False):
"""
Create track in database from passed path, or update database entry
if path already in database.
@ -100,7 +100,7 @@ def create_track_from_file(session, path, interactive=False):
track.mtime = os.path.getmtime(path)
session.commit()
if Config.NORMALISE_ON_IMPORT:
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
if interactive:
INFO("Normalise...")
# Check type

View File

@ -2,11 +2,12 @@
import pytest
import sys
sys.path.append("app")
import models
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
sys.path.append("app")
@pytest.fixture(scope="session")
@ -18,23 +19,6 @@ def connection():
return engine.connect()
def seed_database():
pass
# users = [
# {
# "id": 1,
# "name": "John Doe",
# },
# # ...
# ]
# for user in users:
# db_user = User(**user)
# db_session.add(db_user)
# db_session.commit()
@pytest.fixture(scope="session")
def setup_database(connection):
from app.models import Base # noqa E402

6
poetry.lock generated
View File

@ -943,6 +943,12 @@ pytest = [
{file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"},
{file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"},
]
pytest-profiling = [
{file = "pytest-profiling-1.7.0.tar.gz", hash = "sha256:93938f147662225d2b8bd5af89587b979652426a8a6ffd7e73ec4a23e24b7f29"},
{file = "pytest_profiling-1.7.0-py2.7.egg", hash = "sha256:3b255f9db36cb2dd7536a8e7e294c612c0be7f7850a7d30754878e4315d56600"},
{file = "pytest_profiling-1.7.0-py2.py3-none-any.whl", hash = "sha256:999cc9ac94f2e528e3f5d43465da277429984a1c237ae9818f8cfd0b06acb019"},
{file = "pytest_profiling-1.7.0-py3.6.egg", hash = "sha256:6bce4e2edc04409d2f3158c16750fab8074f62d404cc38eeb075dff7fcbb996c"},
]
pytest-qt = [
{file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"},
{file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"},

View File

@ -126,7 +126,7 @@ def test_playdates_add_playdate(session):
assert playdate
last_played = Playdates.last_played(session, track.id)
assert playdate.lastplayed == last_played
assert abs((playdate.lastplayed - last_played).total_seconds()) < 2
def test_playdates_remove_track(session):
@ -274,6 +274,9 @@ def test_playlist_remove_tracks(session):
playlist1.remove_track(session, 1)
assert len(playlist1.tracks) == 2
# Check the track itself still exists
original_track = Tracks.get_by_id(session, track1.id)
assert original_track
playlist1.remove_all_tracks(session)
assert len(playlist1.tracks) == 0
@ -330,8 +333,7 @@ def test_playlisttracks_move_track(session):
assert tracks[track2_row] == track2
# Move track2 to playlist2 and check
PlaylistTracks.move_track(
session, playlist1.id, track2_row, playlist2.id)
playlist1.move_track(session, [track2_row], playlist2)
tracks1 = playlist1.tracks
tracks2 = playlist2.tracks

View File

@ -1,15 +1,47 @@
from PyQt5.QtCore import Qt
from app.playlists import Notes, PlaylistTab, Tracks
from app.models import Playlists
import musicmuster
from app import playlists
from app import models
from app import musicmuster
from app import dbconfig
def seed2tracks(session):
tracks = [
{
"path": "testdata/isa.mp3",
"title": "I'm so afraid",
"artist": "Fleetwood Mac",
"duration": 263000,
"start_gap": 60,
"fade_at": 236263,
"silence_at": 260343,
"mtime": 371900000,
},
{
"path": "testdata/mom.mp3",
"title": "Man of Mystery",
"artist": "The Shadows",
"duration": 120000,
"start_gap": 70,
"fade_at": 115000,
"silence_at": 118000,
"mtime": 1642760000,
},
]
for track in tracks:
db_track = models.Tracks(session=session, **track)
session.add(db_track)
session.commit()
def test_init(qtbot, session):
"""Just check we can create a playlist_tab"""
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist.id)
playlist = models.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
assert playlist_tab
@ -17,46 +49,51 @@ def test_save_and_restore(qtbot, session):
"""Playlist with one track, one note, save and restore"""
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist.id)
playlist = models.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
# Insert a note
note_text = "my note"
note_row = 7
note = Notes(session, playlist.id, note_row, note_text)
note = models.Notes(session, playlist.id, note_row, note_text)
playlist_tab._insert_note(session, note)
# Add a track
track_path = "/a/b/c"
track = Tracks(session, track_path)
track = models.Tracks(session, track_path)
playlist_tab.insert_track(session, track)
# Save playlist
playlist_tab.save_playlist(session)
# We need to commit the session before re-querying
session.commit()
# Retrieve playlist
playlists = Playlists.get_open(session)
assert len(playlists) == 1
retrieved_playlist = playlists[0]
assert track_path in [a.path for a in retrieved_playlist.tracks.values()]
assert note_text in [a.note for a in retrieved_playlist.notes]
all_playlists = playlists.Playlists.get_open(session)
assert len(all_playlists) == 1
retrieved_playlist = all_playlists[0]
paths = [a.path for a in retrieved_playlist.tracks.values()]
assert track_path in paths
notes = [a.note for a in retrieved_playlist.notes]
assert note_text in notes
def test_meta_all_clear(qtbot, session):
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist.id)
playlist = models.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
track1 = models.Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
track2 = models.Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
track3_path = "/h/i/j"
track3 = Tracks(session, track3_path)
track3 = models.Tracks(session, track3_path)
playlist_tab.insert_track(session, track3)
assert playlist_tab._get_current_track_row() is None
@ -69,18 +106,18 @@ def test_meta_all_clear(qtbot, session):
def test_meta(qtbot, session):
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist.id)
playlist = playlists.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
track1 = models.Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
track2 = models.Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
track3_path = "/h/i/j"
track3 = Tracks(session, track3_path)
track3 = models.Tracks(session, track3_path)
playlist_tab.insert_track(session, track3)
assert len(playlist_tab._get_unreadable_track_rows()) == 3
@ -99,7 +136,7 @@ def test_meta(qtbot, session):
# Add a note
note_text = "my note"
note_row = 7 # will be added as row 3
note = Notes(session, playlist.id, note_row, note_text)
note = models.Notes(session, playlist.id, note_row, note_text)
playlist_tab._insert_note(session, note)
assert playlist_tab._get_played_track_rows() == [0]
@ -147,15 +184,15 @@ def test_meta(qtbot, session):
def test_clear_next(qtbot, session):
# Create playlist
playlist = Playlists(session, "my playlist")
playlist_tab = PlaylistTab(None, session, playlist.id)
playlist = models.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
track1 = models.Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
track2 = models.Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
playlist_tab._set_next_track_row(1)
@ -165,21 +202,24 @@ def test_clear_next(qtbot, session):
assert playlist_tab._get_next_track_row() is None
def test_get_selected_row(qtbot, session):
def test_get_selected_row(qtbot, monkeypatch, session):
# Create playlist
playlist = Playlists(session, "test playlist")
playlist_tab = PlaylistTab(None, session, playlist.id)
monkeypatch.setattr(musicmuster, "Session", session)
monkeypatch.setattr(playlists, "Session", session)
# Create playlist and playlist_tab
window = musicmuster.Window()
playlist = models.Playlists(session, "test playlist")
playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
# Add some tracks
track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
track1 = models.Tracks(session, track1_path)
playlist_tab.insert_track(session, track1)
track2_path = "/d/e/f"
track2 = Tracks(session, track2_path)
track2 = models.Tracks(session, track2_path)
playlist_tab.insert_track(session, track2)
window = Window()
qtbot.addWidget(playlist_tab)
with qtbot.waitExposed(window):
window.show()
@ -191,31 +231,37 @@ def test_get_selected_row(qtbot, session):
)
def test_set_next(qtbot, session):
def test_set_next(qtbot, monkeypatch, session):
monkeypatch.setattr(musicmuster, "Session", session)
monkeypatch.setattr(playlists, "Session", session)
seed2tracks(session)
playlist_name = "test playlist"
# Create testing playlist
playlist = Playlists(session, "test playlist")
playlist_tab = PlaylistTab(None, session, playlist.id)
window = musicmuster.Window()
playlist = models.Playlists(session, playlist_name)
playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
idx = window.tabPlaylist.addTab(playlist_tab, playlist_name)
window.tabPlaylist.setCurrentIndex(idx)
qtbot.addWidget(playlist_tab)
# Add some tracks
track1_path = "testdata/isa.mp3"
track1 = Tracks(session, track1_path)
track1 = models.Tracks.get_from_filename(session, "isa.mp3")
playlist_tab.insert_track(session, track1)
track2_path = "mom.mp3"
track2 = Tracks(session, track2_path)
track2 = models.Tracks.get_from_filename(session, "mom.mp3")
playlist_tab.insert_track(session, track2)
window = Window()
qtbot.addWidget(playlist_tab)
with qtbot.waitExposed(window):
window.show()
row0_item2 = playlist_tab.item(0, 2)
assert row0_item2 is not None
rect = playlist_tab.visualItemRect(row0_item2)
qtbot.mouseClick(
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
)
qtbot.wait(10000)
# qtbot.wait(10000)
qtbot.keyPress(playlist_tab.viewport(), "N",
modifier=Qt.ControlModifier)
qtbot.wait(2000)
@ -223,6 +269,12 @@ def test_set_next(qtbot, session):
def test_kae(monkeypatch, session):
# monkeypatch.setattr(dbconfig, "Session", session)
monkeypatch.setattr(musicmuster, "Session", session)
musicmuster.Window.kae()
# monkeypatch.setattr(musicmuster, "Session", session)
# monkeypatch.setattr(dbconfig, "Session", session)
# monkeypatch.setattr(models, "Session", session)
# monkeypatch.setattr(playlists, "Session", session)