Compare commits

..

No commits in common. "35438f59fb7fd3d6be5c9905522a3b95885f63c5" and "5f1682c0c675122ca890916243ca0737af75abb9" have entirely different histories.

11 changed files with 125 additions and 150 deletions

View File

@ -116,7 +116,6 @@ class PlaylistTrack:
self.end_time: Optional[dt.datetime] = None self.end_time: Optional[dt.datetime] = None
self.fade_at: Optional[int] = None self.fade_at: Optional[int] = None
self.fade_graph: Optional[FadeCurve] = None self.fade_graph: Optional[FadeCurve] = None
self.fade_graph_start_updates: Optional[dt.datetime] = None
self.fade_length: Optional[int] = None self.fade_length: Optional[int] = None
self.path: Optional[str] = None self.path: Optional[str] = None
self.playlist_id: Optional[int] = None self.playlist_id: Optional[int] = None
@ -183,20 +182,10 @@ class PlaylistTrack:
Called when track starts playing Called when track starts playing
""" """
now = dt.datetime.now() self.start_time = dt.datetime.now()
self.start_time = now
if self.duration: if self.duration:
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration) self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
# Calculate time fade_graph should start updating
if self.fade_at:
update_graph_at_ms = max(
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
)
self.fade_graph_start_updates = now + dt.timedelta(
milliseconds=update_graph_at_ms
)
class AddFadeCurve(QObject): class AddFadeCurve(QObject):
""" """

View File

@ -1,11 +1,13 @@
# Standard library imports # Standard library imports
import os
import sys
from typing import List, Optional from typing import List, Optional
import datetime as dt import datetime as dt
# PyQt imports # PyQt imports
# Third party imports # Third party imports
from alchemical import Model # type: ignore from alchemical import Alchemical, Model # type: ignore
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
DateTime, DateTime,

View File

@ -1,15 +1,17 @@
# Standard library imports # Standard library imports
from typing import Optional
# PyQt imports # PyQt imports
# Third party imports
# App imports
from typing import Optional
from PyQt6.QtCore import QEvent, Qt from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtWidgets import QDialog, QListWidgetItem from PyQt6.QtWidgets import QDialog, QListWidgetItem
# Third party imports
from sqlalchemy.orm.session import Session
# App imports
from classes import MusicMusterSignals from classes import MusicMusterSignals
from sqlalchemy.orm import scoped_session
from helpers import ( from helpers import (
ask_yes_no, ask_yes_no,
get_relative_date, get_relative_date,
@ -26,7 +28,7 @@ class TrackSelectDialog(QDialog):
def __init__( def __init__(
self, self,
session: Session, session: scoped_session,
new_row_number: int, new_row_number: int,
source_model: PlaylistModel, source_model: PlaylistModel,
add_to_header: Optional[bool] = False, add_to_header: Optional[bool] = False,

View File

@ -44,7 +44,7 @@ class FadeTrack(QRunnable):
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND) sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.player.stop() self.player.stop()
log.debug(f"Releasing player {self.player=}") log.error(f"Releasing player {self.player=}")
self.player.release() self.player.release()

View File

@ -48,7 +48,7 @@ from PyQt6.QtWidgets import (
# Third party imports # Third party imports
from pygame import mixer from pygame import mixer
import pipeclient import pipeclient
from sqlalchemy.orm.session import Session from sqlalchemy.orm import scoped_session
import stackprinter # type: ignore import stackprinter # type: ignore
# App imports # App imports
@ -562,7 +562,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer1000.timeout.connect(self.tick_1000ms) self.timer1000.timeout.connect(self.tick_1000ms)
def create_playlist( def create_playlist(
self, session: Session, playlist_name: Optional[str] = None self, session: scoped_session, playlist_name: Optional[str] = None
) -> Optional[Playlists]: ) -> Optional[Playlists]:
"""Create new playlist""" """Create new playlist"""
@ -1140,25 +1140,34 @@ class Window(QMainWindow, Ui_MainWindow):
break break
sleep(0.1) sleep(0.1)
# TODO: remove sleep() calls - used to try to isolate bug #223
# Show closing volume graph # Show closing volume graph
sleep(1)
if track_sequence.now.fade_graph: if track_sequence.now.fade_graph:
track_sequence.now.fade_graph.plot() track_sequence.now.fade_graph.plot()
else: else:
log.error("No fade_graph") log.error("No fade_graph")
# Note that track is playing # Note that track is playing
log.debug("set track_sequence") sleep(1)
log.error("set track_sequence")
track_sequence.now.start() track_sequence.now.start()
self.playing = True self.playing = True
# Disable play next controls # Disable play next controls
sleep(1)
log.error("catch return key")
self.catch_return_key = True self.catch_return_key = True
self.show_status_message("Play controls: Disabled", 0) self.show_status_message("Play controls: Disabled", 0)
# Notify model # Notify model
sleep(1)
log.error("active_proxy_model().current_track_started()")
self.active_proxy_model().current_track_started() self.active_proxy_model().current_track_started()
# Update headers # Update headers
sleep(1)
log.error("update headers")
self.update_headers() self.update_headers()
def preview(self) -> None: def preview(self) -> None:
@ -1404,7 +1413,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.currentWidget().scroll_to_top(display_row) self.tabPlaylist.currentWidget().scroll_to_top(display_row)
def solicit_playlist_name( def solicit_playlist_name(
self, session: Session, default: str = "" self, session: scoped_session, default: str = ""
) -> Optional[str]: ) -> Optional[str]:
"""Get name of new playlist from user""" """Get name of new playlist from user"""
@ -1505,12 +1514,6 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Update volume fade curve # Update volume fade curve
if (
track_sequence.now.fade_graph_start_updates is None
or track_sequence.now.fade_graph_start_updates > dt.datetime.now()
):
return
if ( if (
track_sequence.now.track_id track_sequence.now.track_id
and track_sequence.now.fade_graph and track_sequence.now.fade_graph
@ -1639,7 +1642,7 @@ class CartDialog(QDialog):
"""Edit cart details""" """Edit cart details"""
def __init__( def __init__(
self, musicmuster: Window, session: Session, cart: Carts, *args, **kwargs self, musicmuster: Window, session: scoped_session, cart: Carts, *args, **kwargs
) -> None: ) -> None:
""" """
Manage carts Manage carts

View File

@ -160,7 +160,7 @@ class PipeClient:
def _write_pipe_open(self) -> None: def _write_pipe_open(self) -> None:
"""Open _write_pipe.""" """Open _write_pipe."""
self._write_pipe = open(WRITE_NAME, "w") self._write_pipe = open(WRITE_NAME, 'w')
def _read_thread_start(self) -> None: def _read_thread_start(self) -> None:
"""Start read_pipe thread.""" """Start read_pipe thread."""
@ -204,8 +204,8 @@ class PipeClient:
"""Read FIFO in worker thread.""" """Read FIFO in worker thread."""
# Thread will wait at this read until it connects. # Thread will wait at this read until it connects.
# Connection should occur as soon as _write_pipe has connected. # Connection should occur as soon as _write_pipe has connected.
with open(READ_NAME, "r") as read_pipe: with open(READ_NAME, 'r') as read_pipe:
message = "" message = ''
pipe_ok = True pipe_ok = True
while pipe_ok: while pipe_ok:
line = read_pipe.readline() line = read_pipe.readline()

View File

@ -279,16 +279,21 @@ class PlaylistModel(QAbstractTableModel):
) )
return return
log.debug("Call OBS scene change") # TODO: remove sleep/log calls, used to debug #223
# Check for OBS scene change
sleep(1)
log.error("Call OBS scene change")
self.obs_scene_change(row_number) self.obs_scene_change(row_number)
with db.Session() as session: with db.Session() as session:
# Update Playdates in database # Update Playdates in database
log.debug("update playdates") sleep(1)
log.error("update playdates")
Playdates(session, track_sequence.now.track_id) Playdates(session, track_sequence.now.track_id)
# Mark track as played in playlist # Mark track as played in playlist
log.debug("Mark track as played") sleep(1)
log.error("Mark track as played")
plr = session.get(PlaylistRows, track_sequence.now.plr_id) plr = session.get(PlaylistRows, track_sequence.now.plr_id)
if plr: if plr:
plr.played = True plr.played = True
@ -297,7 +302,8 @@ class PlaylistModel(QAbstractTableModel):
log.error(f"Can't retrieve plr, {track_sequence.now.plr_id=}") log.error(f"Can't retrieve plr, {track_sequence.now.plr_id=}")
# Update track times # Update track times
log.debug("Update track times") sleep(1)
log.error("Update track times")
if prd: if prd:
prd.start_time = track_sequence.now.start_time prd.start_time = track_sequence.now.start_time
prd.end_time = track_sequence.now.end_time prd.end_time = track_sequence.now.end_time
@ -314,7 +320,8 @@ class PlaylistModel(QAbstractTableModel):
# Find next track # Find next track
# Get all unplayed track rows # Get all unplayed track rows
log.debug("Find next track") sleep(1)
log.error("Find next track")
next_row = None next_row = None
unplayed_rows = self.get_unplayed_rows() unplayed_rows = self.get_unplayed_rows()
if unplayed_rows: if unplayed_rows:
@ -1215,7 +1222,7 @@ class PlaylistModel(QAbstractTableModel):
self.signals.next_track_changed_signal.emit() self.signals.next_track_changed_signal.emit()
return return
# Update track_sequence # Update playing_track
with db.Session() as session: with db.Session() as session:
track_sequence.next = PlaylistTrack() track_sequence.next = PlaylistTrack()
try: try:
@ -1569,7 +1576,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
self, self,
proposed_row_number: Optional[int], proposed_row_number: Optional[int],
track_id: Optional[int] = None, track_id: Optional[int] = None,
note: str = "", note: Optional[str] = None,
) -> None: ) -> None:
return self.source_model.insert_row(proposed_row_number, track_id, note) return self.source_model.insert_row(proposed_row_number, track_id, note)

View File

@ -4,25 +4,20 @@
# the current directory contains a "better" version of the file than the # the current directory contains a "better" version of the file than the
# parent (eg, bettet bitrate). # parent (eg, bettet bitrate).
# Standard library imports
import os import os
import pydymenu # type: ignore
import shutil import shutil
import sys import sys
from typing import List
# PyQt imports
# Third party imports
import pydymenu # type: ignore
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session
# App imports
from helpers import ( from helpers import (
get_tags, get_tags,
set_track_metadata, set_track_metadata,
) )
from models import db, Tracks
from models import Tracks
from dbconfig import Session
from sqlalchemy.exc import IntegrityError
from typing import List
# ###################### SETTINGS ######################### # ###################### SETTINGS #########################
process_name_and_tags_matches = True process_name_and_tags_matches = True
@ -47,7 +42,7 @@ def main():
# We only want to run this against the production database because # We only want to run this against the production database because
# we will affect files in the common pool of tracks used by all # we will affect files in the common pool of tracks used by all
# databases # databases
if "musicmuster_prod" not in os.environ.get("ALCHEMICAL_DATABASE_URI"): if "musicmuster_prod" not in os.environ.get("MM_DB"):
response = input("Not on production database - c to continue: ") response = input("Not on production database - c to continue: ")
if response != "c": if response != "c":
sys.exit(0) sys.exit(0)
@ -56,7 +51,7 @@ def main():
assert source_dir != parent_dir assert source_dir != parent_dir
# Scan parent directory # Scan parent directory
with db.Session() as session: with Session() as session:
all_tracks = Tracks.get_all(session) all_tracks = Tracks.get_all(session)
parent_tracks = [a for a in all_tracks if parent_dir in a.path] parent_tracks = [a for a in all_tracks if parent_dir in a.path]
parent_fnames = [os.path.basename(a.path) for a in parent_tracks] parent_fnames = [os.path.basename(a.path) for a in parent_tracks]
@ -244,7 +239,7 @@ def process_track(src, dst, title, artist, bitrate):
if not do_processing: if not do_processing:
return return
with db.Session() as session: with Session() as session:
track = Tracks.get_by_path(session, dst) track = Tracks.get_by_path(session, dst)
if track: if track:
# Update path, but workaround MariaDB bug # Update path, but workaround MariaDB bug

View File

@ -59,7 +59,7 @@ explicit_package_bases = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "--exitfirst --showlocals --capture=no" addopts = "--exitfirst --showlocals --capture=no"
pythonpath = [".", "app"] pythonpath = [".", "app"]
filterwarnings = ["ignore:'audioop' is deprecated", "ignore:pkg_resources"] filterwarnings = "ignore:'audioop' is deprecated"
[tool.vulture] [tool.vulture]
exclude = ["migrations", "app/ui", "archive"] exclude = ["migrations", "app/ui", "archive"]

View File

@ -79,13 +79,12 @@ class TestMMMiscTracks(unittest.TestCase):
self.model.insert_row(proposed_row_number=END_ROW, note="-") self.model.insert_row(proposed_row_number=END_ROW, note="-")
prd = self.model.playlist_rows[START_ROW] prd = self.model.playlist_rows[START_ROW]
qv_value = self.model.display_role( qv_value = self.model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd)
START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd
)
assert qv_value.value() == "start [1 tracks, 4:23 unplayed]" assert qv_value.value() == "start [1 tracks, 4:23 unplayed]"
class TestMMMiscNoPlaylist(unittest.TestCase): class TestMMMiscNoPlaylist(unittest.TestCase):
PLAYLIST_NAME = "tracks playlist" PLAYLIST_NAME = "tracks playlist"
test_tracks = [ test_tracks = [
"testdata/isa.mp3", "testdata/isa.mp3",
@ -122,13 +121,10 @@ class TestMMMiscNoPlaylist(unittest.TestCase):
_ = str(prd) _ = str(prd)
assert ( assert (
model.edit_role( model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd)
model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd
)
== metadata["title"] == metadata["title"]
) )
class TestMMMiscRowMove(unittest.TestCase): class TestMMMiscRowMove(unittest.TestCase):
PLAYLIST_NAME = "rowmove playlist" PLAYLIST_NAME = "rowmove playlist"
ROWS_TO_CREATE = 11 ROWS_TO_CREATE = 11
@ -296,6 +292,7 @@ class TestMMMiscRowMove(unittest.TestCase):
self.model.add_track_to_header(insert_row, prd.track_id) self.model.add_track_to_header(insert_row, prd.track_id)
def test_reverse_row_groups_one_row(self): def test_reverse_row_groups_one_row(self):
rows_to_move = [3] rows_to_move = [3]
result = self.model._reversed_contiguous_row_groups(rows_to_move) result = self.model._reversed_contiguous_row_groups(rows_to_move)
@ -304,6 +301,7 @@ class TestMMMiscRowMove(unittest.TestCase):
assert result[0] == [3] assert result[0] == [3]
def test_reverse_row_groups_multiple_row(self): def test_reverse_row_groups_multiple_row(self):
rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21] rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21]
result = self.model._reversed_contiguous_row_groups(rows_to_move) result = self.model._reversed_contiguous_row_groups(rows_to_move)
@ -359,6 +357,7 @@ class TestMMMiscRowMove(unittest.TestCase):
assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4, 5, 6, 7, 8, 9, 10] assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def test_move_multiple_rows_between_playlists_to_end(self): def test_move_multiple_rows_between_playlists_to_end(self):
from_rows = [1, 3, 4] from_rows = [1, 3, 4]
to_row = 2 to_row = 2
destination_playlist = "destination" destination_playlist = "destination"
@ -383,22 +382,7 @@ class TestMMMiscRowMove(unittest.TestCase):
assert len(model_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows) assert len(model_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows)
assert len(model_dst.playlist_rows) == self.ROWS_TO_CREATE + len(from_rows) assert len(model_dst.playlist_rows) == self.ROWS_TO_CREATE + len(from_rows)
assert [int(a) for a in row_notes] == [ assert [int(a) for a in row_notes] == [0, 1, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
0,
1,
3,
4,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
]
# # def test_edit_header(monkeypatch, session): # edit header row in middle of playlist # # def test_edit_header(monkeypatch, session): # edit header row in middle of playlist

View File

@ -18,7 +18,6 @@ DB_FILE = "/tmp/mm.db"
if os.path.exists(DB_FILE): if os.path.exists(DB_FILE):
os.unlink(DB_FILE) os.unlink(DB_FILE)
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
from app import playlistmodel, utilities
from app.models import ( # noqa: E402 from app.models import ( # noqa: E402
db, db,
Carts, Carts,
@ -42,14 +41,13 @@ def qtbot_adapter(qapp, request):
# Wrapper to handle setup/teardown operations # Wrapper to handle setup/teardown operations
def with_updown(function): def with_updown(function):
def test_wrapper(self, *args, **kwargs): def test_wrapper(self, *args, **kwargs):
if callable(getattr(self, "up", None)): if callable(getattr(self, 'up', None)):
self.up() self.up()
try: try:
function(self, *args, **kwargs) function(self, *args, **kwargs)
finally: finally:
if callable(getattr(self, "down", None)): if callable(getattr(self, 'down', None)):
self.down() self.down()
test_wrapper.__doc__ = function.__doc__ test_wrapper.__doc__ = function.__doc__
return test_wrapper return test_wrapper
@ -60,41 +58,7 @@ class MyTestCase(unittest.TestCase):
def up(self): def up(self):
db.create_all() db.create_all()
self.widget = musicmuster.Window() self.widget = musicmuster.Window()
# self.widget.show() self.widget.show()
# Add two tracks to database
self.tracks = {
1: {
"path": "testdata/isa.mp3",
"title": "I'm so afraid",
"artist": "Fleetwood Mac",
"bitrate": 64,
"duration": 263000,
"start_gap": 60,
"fade_at": 236263,
"silence_at": 260343,
"mtime": 371900000,
},
2: {
"path": "testdata/mom.mp3",
"title": "Man of Mystery",
"artist": "The Shadows",
"bitrate": 64,
"duration": 120000,
"start_gap": 70,
"fade_at": 115000,
"silence_at": 118000,
"mtime": 1642760000,
},
}
with db.Session() as session:
for track in self.tracks.values():
db_track = Tracks(session=session, **track)
session.add(db_track)
track['id'] = db_track.id
session.commit()
def down(self): def down(self):
db.drop_all() db.drop_all()
@ -103,52 +67,81 @@ class MyTestCase(unittest.TestCase):
def test_init(self): def test_init(self):
"""Just check we can create a playlist_tab""" """Just check we can create a playlist_tab"""
playlist_name = "test_init playlist"
with db.Session() as session: with db.Session() as session:
playlist = Playlists(session, playlist_name) playlist = Playlists(session, "test playlist")
# playlist_tab = playlists.PlaylistTab(self.widget, playlist.id)
self.widget.create_playlist_tab(playlist) self.widget.create_playlist_tab(playlist)
with self.qtbot.waitExposed(self.widget): with self.qtbot.waitExposed(self.widget):
# window.show()
# with self.qtbot.waitSignal(self.widget.my_signal, timeout=300):
# with qtbot.waitExposed(window):
self.widget.show() self.widget.show()
@with_updown
def test_save_and_restore(self):
"""Playlist with one track, one note, save and restore"""
note_text = "my note" # def seed2tracks(session):
playlist_name = "test_save_and_restore playlist" # 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,
# },
# ]
with db.Session() as session: # for track in tracks:
playlist = Playlists(session, playlist_name) # db_track = models.Tracks(session=session, **track)
model = playlistmodel.PlaylistModel(playlist.id) # session.add(db_track)
# Add a track with a note # session.commit()
model.insert_row(proposed_row_number=0, track_id=self.tracks[1]['id'], note=note_text)
# We need to commit the session before re-querying
session.commit()
# Retrieve playlist
all_playlists = Playlists.get_all(session)
assert len(all_playlists) == 1
retrieved_playlist = all_playlists[0]
assert len(retrieved_playlist.rows) == 1
paths = [a.track.path for a in retrieved_playlist.rows]
assert self.tracks[1]['path'] in paths
notes = [a.note for a in retrieved_playlist.rows]
assert note_text in notes
@with_updown # def test_save_and_restore(qtbot, session):
def test_utilities(self): # """Playlist with one track, one note, save and restore"""
"""Test check_db utility"""
from config import Config # # Create playlist
# playlist = models.Playlists(session, "my playlist")
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
Config.ROOT = os.path.join(os.path.dirname(__file__), 'testdata') # # Insert a note
# note_text = "my note"
# note_row = 7
# 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 = models.Tracks(session, track_path)
# # Inserting the track will also save the playlist
# playlist_tab.insert_track(session, track)
# # We need to commit the session before re-querying
# session.commit()
# # Retrieve playlist
# 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
with db.Session() as session:
utilities.check_db(session)
utilities.update_bitrates(session)
# def test_meta_all_clear(qtbot, session): # def test_meta_all_clear(qtbot, session):
# # Create playlist # # Create playlist