Merge v4 branch
This commit is contained in:
commit
b8c19c6046
6
.envrc
6
.envrc
@ -9,12 +9,12 @@ branch=$(git branch --show-current)
|
|||||||
# Always treat running from /home/kae/mm as production
|
# Always treat running from /home/kae/mm as production
|
||||||
if [ $(pwd) == /home/kae/mm ]; then
|
if [ $(pwd) == /home/kae/mm ]; then
|
||||||
export MM_ENV="PRODUCTION"
|
export MM_ENV="PRODUCTION"
|
||||||
export MM_DB="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
export ALCHEMICAL_DATABASE_URI="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
||||||
elif on_git_branch master; then
|
elif on_git_branch master; then
|
||||||
export MM_ENV="PRODUCTION"
|
export MM_ENV="PRODUCTION"
|
||||||
export MM_DB="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
export ALCHEMICAL_DATABASE_URI="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod"
|
||||||
else
|
else
|
||||||
export MM_ENV="DEVELOPMENT"
|
export MM_ENV="DEVELOPMENT"
|
||||||
export MM_DB="mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster"
|
export ALCHEMICAL_DATABASE_URI="mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster"
|
||||||
export PYTHONBREAKPOINT="pudb.set_trace"
|
export PYTHONBREAKPOINT="pudb.set_trace"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -53,7 +53,6 @@ class MyTableWidget(QTableView):
|
|||||||
|
|
||||||
|
|
||||||
class MyModel(QAbstractTableModel):
|
class MyModel(QAbstractTableModel):
|
||||||
|
|
||||||
def columnCount(self, index):
|
def columnCount(self, index):
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
@ -71,7 +70,11 @@ class MyModel(QAbstractTableModel):
|
|||||||
return QVariant()
|
return QVariant()
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable
|
return (
|
||||||
|
Qt.ItemFlag.ItemIsEnabled
|
||||||
|
| Qt.ItemFlag.ItemIsSelectable
|
||||||
|
| Qt.ItemFlag.ItemIsEditable
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
|
|||||||
@ -1,286 +0,0 @@
|
|||||||
from PyQt6.QtCore import Qt
|
|
||||||
|
|
||||||
from app import playlists
|
|
||||||
from app import models
|
|
||||||
from app import musicmuster
|
|
||||||
|
|
||||||
|
|
||||||
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 = models.Playlists(session, "my playlist")
|
|
||||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
|
||||||
assert playlist_tab
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_and_restore(qtbot, session):
|
|
||||||
"""Playlist with one track, one note, save and restore"""
|
|
||||||
|
|
||||||
# Create playlist
|
|
||||||
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 = 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
|
|
||||||
|
|
||||||
|
|
||||||
def test_meta_all_clear(qtbot, session):
|
|
||||||
# Create playlist
|
|
||||||
playlist = models.Playlists(session, "my playlist")
|
|
||||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
|
||||||
|
|
||||||
# Add some tracks
|
|
||||||
# Need to commit session after each one so that new row is found
|
|
||||||
# for subsequent inserts
|
|
||||||
track1_path = "/a/b/c"
|
|
||||||
track1 = models.Tracks(session, track1_path)
|
|
||||||
playlist_tab.insert_track(session, track1)
|
|
||||||
session.commit()
|
|
||||||
track2_path = "/d/e/f"
|
|
||||||
track2 = models.Tracks(session, track2_path)
|
|
||||||
playlist_tab.insert_track(session, track2)
|
|
||||||
session.commit()
|
|
||||||
track3_path = "/h/i/j"
|
|
||||||
track3 = models.Tracks(session, track3_path)
|
|
||||||
playlist_tab.insert_track(session, track3)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
assert playlist_tab._get_current_track_row() is None
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
assert playlist_tab._get_notes_rows() == []
|
|
||||||
assert playlist_tab._get_played_track_rows() == []
|
|
||||||
assert len(playlist_tab._get_unreadable_track_rows()) == 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_meta(qtbot, session):
|
|
||||||
# Create playlist
|
|
||||||
playlist = playlists.Playlists(session, "my playlist")
|
|
||||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
|
||||||
|
|
||||||
# Add some tracks
|
|
||||||
track1_path = "/a/b/c"
|
|
||||||
track1 = models.Tracks(session, track1_path)
|
|
||||||
playlist_tab.insert_track(session, track1)
|
|
||||||
session.commit()
|
|
||||||
track2_path = "/d/e/f"
|
|
||||||
track2 = models.Tracks(session, track2_path)
|
|
||||||
playlist_tab.insert_track(session, track2)
|
|
||||||
session.commit()
|
|
||||||
track3_path = "/h/i/j"
|
|
||||||
track3 = models.Tracks(session, track3_path)
|
|
||||||
playlist_tab.insert_track(session, track3)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
assert len(playlist_tab._get_unreadable_track_rows()) == 3
|
|
||||||
|
|
||||||
assert playlist_tab._get_played_track_rows() == []
|
|
||||||
assert playlist_tab._get_current_track_row() is None
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
assert playlist_tab._get_notes_rows() == []
|
|
||||||
|
|
||||||
playlist_tab._set_played_row(0)
|
|
||||||
assert playlist_tab._get_played_track_rows() == [0]
|
|
||||||
assert playlist_tab._get_current_track_row() is None
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
assert playlist_tab._get_notes_rows() == []
|
|
||||||
|
|
||||||
# Add a note
|
|
||||||
note_text = "my note"
|
|
||||||
note_row = 7 # will be added as row 3
|
|
||||||
note = models.Notes(session, playlist.id, note_row, note_text)
|
|
||||||
playlist_tab._insert_note(session, note)
|
|
||||||
|
|
||||||
assert playlist_tab._get_played_track_rows() == [0]
|
|
||||||
assert playlist_tab._get_current_track_row() is None
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
assert playlist_tab._get_notes_rows() == [3]
|
|
||||||
|
|
||||||
playlist_tab._set_next_track_row(1)
|
|
||||||
assert playlist_tab._get_played_track_rows() == [0]
|
|
||||||
assert playlist_tab._get_current_track_row() is None
|
|
||||||
assert playlist_tab._get_next_track_row() == 1
|
|
||||||
assert playlist_tab._get_notes_rows() == [3]
|
|
||||||
|
|
||||||
playlist_tab._set_current_track_row(2)
|
|
||||||
assert playlist_tab._get_played_track_rows() == [0]
|
|
||||||
assert playlist_tab._get_current_track_row() == 2
|
|
||||||
assert playlist_tab._get_next_track_row() == 1
|
|
||||||
assert playlist_tab._get_notes_rows() == [3]
|
|
||||||
|
|
||||||
playlist_tab._clear_played_row_status(0)
|
|
||||||
assert playlist_tab._get_played_track_rows() == []
|
|
||||||
assert playlist_tab._get_current_track_row() == 2
|
|
||||||
assert playlist_tab._get_next_track_row() == 1
|
|
||||||
assert playlist_tab._get_notes_rows() == [3]
|
|
||||||
|
|
||||||
playlist_tab._meta_clear_next()
|
|
||||||
assert playlist_tab._get_played_track_rows() == []
|
|
||||||
assert playlist_tab._get_current_track_row() == 2
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
assert playlist_tab._get_notes_rows() == [3]
|
|
||||||
|
|
||||||
playlist_tab._clear_current_track_row()
|
|
||||||
assert playlist_tab._get_played_track_rows() == []
|
|
||||||
assert playlist_tab._get_current_track_row() is None
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
assert playlist_tab._get_notes_rows() == [3]
|
|
||||||
|
|
||||||
# Test clearing again has no effect
|
|
||||||
playlist_tab._clear_current_track_row()
|
|
||||||
assert playlist_tab._get_played_track_rows() == []
|
|
||||||
assert playlist_tab._get_current_track_row() is None
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
assert playlist_tab._get_notes_rows() == [3]
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_next(qtbot, session):
|
|
||||||
# Create playlist
|
|
||||||
playlist = models.Playlists(session, "my playlist")
|
|
||||||
playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
|
||||||
|
|
||||||
# Add some tracks
|
|
||||||
track1_path = "/a/b/c"
|
|
||||||
track1 = models.Tracks(session, track1_path)
|
|
||||||
playlist_tab.insert_track(session, track1)
|
|
||||||
session.commit()
|
|
||||||
track2_path = "/d/e/f"
|
|
||||||
track2 = models.Tracks(session, track2_path)
|
|
||||||
playlist_tab.insert_track(session, track2)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
playlist_tab._set_next_track_row(1)
|
|
||||||
assert playlist_tab._get_next_track_row() == 1
|
|
||||||
|
|
||||||
playlist_tab.clear_next(session)
|
|
||||||
assert playlist_tab._get_next_track_row() is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_selected_row(qtbot, monkeypatch, session):
|
|
||||||
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 = models.Tracks(session, track1_path)
|
|
||||||
playlist_tab.insert_track(session, track1)
|
|
||||||
session.commit()
|
|
||||||
track2_path = "/d/e/f"
|
|
||||||
track2 = models.Tracks(session, track2_path)
|
|
||||||
playlist_tab.insert_track(session, track2)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
qtbot.addWidget(playlist_tab)
|
|
||||||
with qtbot.waitExposed(window):
|
|
||||||
window.show()
|
|
||||||
row0_item0 = playlist_tab.item(0, 0)
|
|
||||||
assert row0_item0 is not None
|
|
||||||
rect = playlist_tab.visualItemRect(row0_item0)
|
|
||||||
qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
|
|
||||||
row_number = playlist_tab.get_selected_row()
|
|
||||||
assert row_number == 0
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
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 = models.Tracks.get_by_filename(session, "isa.mp3")
|
|
||||||
track1_title = track1.title
|
|
||||||
assert track1_title
|
|
||||||
|
|
||||||
playlist_tab.insert_track(session, track1)
|
|
||||||
session.commit()
|
|
||||||
track2 = models.Tracks.get_by_filename(session, "mom.mp3")
|
|
||||||
playlist_tab.insert_track(session, track2)
|
|
||||||
|
|
||||||
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())
|
|
||||||
selected_title = playlist_tab.get_selected_title()
|
|
||||||
assert selected_title == track1_title
|
|
||||||
|
|
||||||
qtbot.keyPress(playlist_tab.viewport(), "N", modifier=Qt.ControlModifier)
|
|
||||||
qtbot.wait(1000)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
@ -1,13 +1,18 @@
|
|||||||
|
# Standard library imports
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
from PyQt6.QtCore import pyqtSignal, QObject, QThread
|
from PyQt6.QtCore import pyqtSignal, QObject, QThread
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg # type: ignore
|
import pyqtgraph as pg # type: ignore
|
||||||
|
from sqlalchemy.orm import scoped_session
|
||||||
|
|
||||||
|
# App imports
|
||||||
from config import Config
|
from config import Config
|
||||||
from dbconfig import scoped_session
|
|
||||||
from models import PlaylistRows
|
from models import PlaylistRows
|
||||||
import helpers
|
import helpers
|
||||||
|
|
||||||
@ -108,9 +113,10 @@ class PlaylistTrack:
|
|||||||
|
|
||||||
self.artist: Optional[str] = None
|
self.artist: Optional[str] = None
|
||||||
self.duration: Optional[int] = None
|
self.duration: Optional[int] = None
|
||||||
self.end_time: Optional[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
|
||||||
@ -119,7 +125,7 @@ class PlaylistTrack:
|
|||||||
self.resume_marker: Optional[float] = None
|
self.resume_marker: Optional[float] = None
|
||||||
self.silence_at: Optional[int] = None
|
self.silence_at: Optional[int] = None
|
||||||
self.start_gap: Optional[int] = None
|
self.start_gap: Optional[int] = None
|
||||||
self.start_time: Optional[datetime] = None
|
self.start_time: Optional[dt.datetime] = None
|
||||||
self.title: Optional[str] = None
|
self.title: Optional[str] = None
|
||||||
self.track_id: Optional[int] = None
|
self.track_id: Optional[int] = None
|
||||||
|
|
||||||
@ -177,9 +183,19 @@ class PlaylistTrack:
|
|||||||
Called when track starts playing
|
Called when track starts playing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.start_time = datetime.now()
|
now = dt.datetime.now()
|
||||||
|
self.start_time = now
|
||||||
if self.duration:
|
if self.duration:
|
||||||
self.end_time = self.start_time + 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):
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import datetime
|
import datetime as dt
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
@ -35,10 +35,10 @@ class Config(object):
|
|||||||
COLOUR_WARNING_TIMER = "#ffc107"
|
COLOUR_WARNING_TIMER = "#ffc107"
|
||||||
DBFS_SILENCE = -50
|
DBFS_SILENCE = -50
|
||||||
DEBUG_FUNCTIONS: List[Optional[str]] = []
|
DEBUG_FUNCTIONS: List[Optional[str]] = []
|
||||||
DEBUG_MODULES: List[Optional[str]] = ["dbconfig"]
|
DEBUG_MODULES: List[Optional[str]] = []
|
||||||
DEFAULT_COLUMN_WIDTH = 200
|
DEFAULT_COLUMN_WIDTH = 200
|
||||||
DISPLAY_SQL = False
|
DISPLAY_SQL = False
|
||||||
EPOCH = datetime.datetime(1970, 1, 1)
|
EPOCH = dt.datetime(1970, 1, 1)
|
||||||
ERRORS_FROM = ["noreply@midnighthax.com"]
|
ERRORS_FROM = ["noreply@midnighthax.com"]
|
||||||
ERRORS_TO = ["kae@midnighthax.com"]
|
ERRORS_TO = ["kae@midnighthax.com"]
|
||||||
FADE_CURVE_BACKGROUND = "lightyellow"
|
FADE_CURVE_BACKGROUND = "lightyellow"
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import os
|
|
||||||
from config import Config
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
MYSQL_CONNECT = os.environ.get("MM_DB")
|
|
||||||
if MYSQL_CONNECT is None:
|
|
||||||
raise ValueError("MYSQL_CONNECT is undefined")
|
|
||||||
else:
|
|
||||||
dbname = MYSQL_CONNECT.split("/")[-1]
|
|
||||||
log.debug(f"Database: {dbname}")
|
|
||||||
|
|
||||||
engine = create_engine(
|
|
||||||
MYSQL_CONNECT,
|
|
||||||
echo=Config.DISPLAY_SQL,
|
|
||||||
pool_pre_ping=True,
|
|
||||||
future=True,
|
|
||||||
connect_args={"charset": "utf8mb4"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def Session() -> Generator[scoped_session, None, None]:
|
|
||||||
frame = inspect.stack()[2]
|
|
||||||
file = frame.filename
|
|
||||||
function = frame.function
|
|
||||||
lineno = frame.lineno
|
|
||||||
Session = scoped_session(sessionmaker(bind=engine))
|
|
||||||
log.debug(f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]")
|
|
||||||
yield Session
|
|
||||||
log.debug(f" Session released [{hex(id(Session))}]")
|
|
||||||
Session.commit()
|
|
||||||
Session.close()
|
|
||||||
178
app/dbtables.py
Normal file
178
app/dbtables.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Standard library imports
|
||||||
|
from typing import List, Optional
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from alchemical import Model # type: ignore
|
||||||
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
String,
|
||||||
|
)
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from sqlalchemy.orm import (
|
||||||
|
Mapped,
|
||||||
|
mapped_column,
|
||||||
|
relationship,
|
||||||
|
)
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
|
||||||
|
|
||||||
|
# Database classes
|
||||||
|
class CartsTable(Model):
|
||||||
|
__tablename__ = "carts"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
cart_number: Mapped[int] = mapped_column(unique=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(256), index=True)
|
||||||
|
duration: Mapped[Optional[int]] = mapped_column(index=True)
|
||||||
|
path: Mapped[Optional[str]] = mapped_column(String(2048), index=False)
|
||||||
|
enabled: Mapped[Optional[bool]] = mapped_column(default=False)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Carts(id={self.id}, cart={self.cart_number}, "
|
||||||
|
f"name={self.name}, path={self.path}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NoteColoursTable(Model):
|
||||||
|
__tablename__ = "notecolours"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
substring: Mapped[str] = mapped_column(String(256), index=False)
|
||||||
|
colour: Mapped[str] = mapped_column(String(21), index=False)
|
||||||
|
enabled: Mapped[bool] = mapped_column(default=True, index=True)
|
||||||
|
is_regex: Mapped[bool] = mapped_column(default=False, index=False)
|
||||||
|
is_casesensitive: Mapped[bool] = mapped_column(default=False, index=False)
|
||||||
|
order: Mapped[Optional[int]] = mapped_column(index=True)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<NoteColours(id={self.id}, substring={self.substring}, "
|
||||||
|
f"colour={self.colour}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaydatesTable(Model):
|
||||||
|
__tablename__ = "playdates"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
|
||||||
|
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id"))
|
||||||
|
track: Mapped["TracksTable"] = relationship(
|
||||||
|
"TracksTable", back_populates="playdates"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Playdates(id={self.id}, track_id={self.track_id} "
|
||||||
|
f"lastplayed={self.lastplayed}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistsTable(Model):
|
||||||
|
"""
|
||||||
|
Manage playlists
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "playlists"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(32), unique=True)
|
||||||
|
last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None)
|
||||||
|
tab: Mapped[Optional[int]] = mapped_column(default=None)
|
||||||
|
open: Mapped[bool] = mapped_column(default=False)
|
||||||
|
is_template: Mapped[bool] = mapped_column(default=False)
|
||||||
|
deleted: Mapped[bool] = mapped_column(default=False)
|
||||||
|
rows: Mapped[List["PlaylistRowsTable"]] = relationship(
|
||||||
|
"PlaylistRowsTable",
|
||||||
|
back_populates="playlist",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
order_by="PlaylistRowsTable.plr_rownum",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Playlists(id={self.id}, name={self.name}, "
|
||||||
|
f"is_templatee={self.is_template}, open={self.open}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistRowsTable(Model):
|
||||||
|
__tablename__ = "playlist_rows"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
plr_rownum: Mapped[int]
|
||||||
|
note: Mapped[str] = mapped_column(
|
||||||
|
String(2048), index=False, default="", nullable=False
|
||||||
|
)
|
||||||
|
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"))
|
||||||
|
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
|
||||||
|
track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id"))
|
||||||
|
track: Mapped["TracksTable"] = relationship(
|
||||||
|
"TracksTable",
|
||||||
|
back_populates="playlistrows",
|
||||||
|
)
|
||||||
|
played: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, index=False, default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<PlaylistRow(id={self.id}, playlist_id={self.playlist_id}, "
|
||||||
|
f"track_id={self.track_id}, "
|
||||||
|
f"note={self.note}, plr_rownum={self.plr_rownum}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsTable(Model):
|
||||||
|
"""Manage settings"""
|
||||||
|
|
||||||
|
__tablename__ = "settings"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), unique=True)
|
||||||
|
f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None)
|
||||||
|
f_int: Mapped[Optional[int]] = mapped_column(default=None)
|
||||||
|
f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Settings(id={self.id}, name={self.name}, "
|
||||||
|
f"f_datetime={self.f_datetime}, f_int={self.f_int}, f_string={self.f_string}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TracksTable(Model):
|
||||||
|
__tablename__ = "tracks"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
title: Mapped[str] = mapped_column(String(256), index=True)
|
||||||
|
artist: Mapped[str] = mapped_column(String(256), index=True)
|
||||||
|
bitrate: Mapped[Optional[int]] = mapped_column(default=None)
|
||||||
|
duration: Mapped[int] = mapped_column(index=True)
|
||||||
|
fade_at: Mapped[int] = mapped_column(index=False)
|
||||||
|
mtime: Mapped[float] = mapped_column(index=True)
|
||||||
|
path: Mapped[str] = mapped_column(String(2048), index=False, unique=True)
|
||||||
|
silence_at: Mapped[int] = mapped_column(index=False)
|
||||||
|
start_gap: Mapped[int] = mapped_column(index=False)
|
||||||
|
playlistrows: Mapped[List[PlaylistRowsTable]] = relationship(
|
||||||
|
"PlaylistRowsTable", back_populates="track"
|
||||||
|
)
|
||||||
|
playlists = association_proxy("playlistrows", "playlist")
|
||||||
|
playdates: Mapped[List[PlaydatesTable]] = relationship(
|
||||||
|
"PlaydatesTable",
|
||||||
|
back_populates="track",
|
||||||
|
lazy="joined",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Track(id={self.id}, title={self.title}, "
|
||||||
|
f"artist={self.artist}, path={self.path}>"
|
||||||
|
)
|
||||||
@ -1,10 +1,15 @@
|
|||||||
|
# Standard library imports
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
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 dbconfig import scoped_session
|
|
||||||
from helpers import (
|
from helpers import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
get_relative_date,
|
get_relative_date,
|
||||||
@ -21,7 +26,7 @@ class TrackSelectDialog(QDialog):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: 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,
|
||||||
@ -104,7 +109,9 @@ class TrackSelectDialog(QDialog):
|
|||||||
|
|
||||||
if self.add_to_header:
|
if self.add_to_header:
|
||||||
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
||||||
self.source_model.move_track_to_header(self.new_row_number, existing_prd, note)
|
self.source_model.move_track_to_header(
|
||||||
|
self.new_row_number, existing_prd, note
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.source_model.add_track_to_header(self.new_row_number, track_id)
|
self.source_model.add_track_to_header(self.new_row_number, track_id)
|
||||||
# Close dialog - we can only add one track to a header
|
# Close dialog - we can only add one track to a header
|
||||||
@ -112,7 +119,9 @@ class TrackSelectDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
# Adding a new track row
|
# Adding a new track row
|
||||||
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
|
||||||
self.source_model.move_track_add_note(self.new_row_number, existing_prd, note)
|
self.source_model.move_track_add_note(
|
||||||
|
self.new_row_number, existing_prd, note
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.source_model.insert_row(self.new_row_number, track_id, note)
|
self.source_model.insert_row(self.new_row_number, track_id, note)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
import datetime as dt
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
import functools
|
import functools
|
||||||
@ -99,7 +99,7 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_embedded_time(text: str) -> Optional[datetime]:
|
def get_embedded_time(text: str) -> Optional[dt.datetime]:
|
||||||
"""Return datetime specified as @hh:mm in text"""
|
"""Return datetime specified as @hh:mm in text"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -110,7 +110,7 @@ def get_embedded_time(text: str) -> Optional[datetime]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT)
|
return dt.datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ def get_file_metadata(filepath: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def get_relative_date(
|
def get_relative_date(
|
||||||
past_date: Optional[datetime], reference_date: Optional[datetime] = None
|
past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Return how long before reference_date past_date is as string.
|
Return how long before reference_date past_date is as string.
|
||||||
@ -158,7 +158,7 @@ def get_relative_date(
|
|||||||
if not past_date or past_date == Config.EPOCH:
|
if not past_date or past_date == Config.EPOCH:
|
||||||
return "Never"
|
return "Never"
|
||||||
if not reference_date:
|
if not reference_date:
|
||||||
reference_date = datetime.now()
|
reference_date = dt.datetime.now()
|
||||||
|
|
||||||
# Check parameters
|
# Check parameters
|
||||||
if past_date > reference_date:
|
if past_date > reference_date:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from datetime import datetime
|
import datetime as dt
|
||||||
from slugify import slugify # type: ignore
|
from slugify import slugify # type: ignore
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from PyQt6.QtCore import QUrl # type: ignore
|
from PyQt6.QtCore import QUrl # type: ignore
|
||||||
@ -24,14 +24,14 @@ class InfoTabs(QTabWidget):
|
|||||||
self.signals.search_songfacts_signal.connect(self.open_in_songfacts)
|
self.signals.search_songfacts_signal.connect(self.open_in_songfacts)
|
||||||
self.signals.search_wikipedia_signal.connect(self.open_in_wikipedia)
|
self.signals.search_wikipedia_signal.connect(self.open_in_wikipedia)
|
||||||
# re-use the oldest one later)
|
# re-use the oldest one later)
|
||||||
self.last_update: Dict[QWebEngineView, datetime] = {}
|
self.last_update: Dict[QWebEngineView, dt.datetime] = {}
|
||||||
self.tabtitles: Dict[int, str] = {}
|
self.tabtitles: Dict[int, str] = {}
|
||||||
|
|
||||||
# Create one tab which (for some reason) creates flickering if
|
# Create one tab which (for some reason) creates flickering if
|
||||||
# done later
|
# done later
|
||||||
widget = QWebEngineView()
|
widget = QWebEngineView()
|
||||||
widget.setZoomFactor(Config.WEB_ZOOM_FACTOR)
|
widget.setZoomFactor(Config.WEB_ZOOM_FACTOR)
|
||||||
self.last_update[widget] = datetime.now()
|
self.last_update[widget] = dt.datetime.now()
|
||||||
_ = self.addTab(widget, "")
|
_ = self.addTab(widget, "")
|
||||||
|
|
||||||
def open_in_songfacts(self, title):
|
def open_in_songfacts(self, title):
|
||||||
@ -80,7 +80,7 @@ class InfoTabs(QTabWidget):
|
|||||||
self.setTabText(tab_index, short_title)
|
self.setTabText(tab_index, short_title)
|
||||||
|
|
||||||
widget.setUrl(QUrl(url))
|
widget.setUrl(QUrl(url))
|
||||||
self.last_update[widget] = datetime.now()
|
self.last_update[widget] = dt.datetime.now()
|
||||||
self.tabtitles[tab_index] = url
|
self.tabtitles[tab_index] = url
|
||||||
|
|
||||||
# Show newly updated tab
|
# Show newly updated tab
|
||||||
|
|||||||
340
app/models.py
340
app/models.py
@ -1,68 +1,46 @@
|
|||||||
#!/usr/bin/python3
|
# Standard library imports
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from config import Config
|
|
||||||
from dbconfig import scoped_session
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from pprint import pprint
|
|
||||||
from typing import List, Optional, Sequence
|
from typing import List, Optional, Sequence
|
||||||
|
import datetime as dt
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
# PyQt imports
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from alchemical import Alchemical # type:ignore
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
bindparam,
|
bindparam,
|
||||||
Boolean,
|
|
||||||
DateTime,
|
|
||||||
delete,
|
delete,
|
||||||
ForeignKey,
|
|
||||||
func,
|
func,
|
||||||
select,
|
select,
|
||||||
String,
|
|
||||||
update,
|
update,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from sqlalchemy.orm import (
|
# App imports
|
||||||
DeclarativeBase,
|
import dbtables
|
||||||
joinedload,
|
from config import Config
|
||||||
Mapped,
|
|
||||||
mapped_column,
|
|
||||||
relationship,
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm.exc import (
|
|
||||||
NoResultFound,
|
|
||||||
)
|
|
||||||
from sqlalchemy.exc import (
|
|
||||||
IntegrityError,
|
|
||||||
)
|
|
||||||
from log import log
|
from log import log
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
# Establish database connection
|
||||||
pass
|
ALCHEMICAL_DATABASE_URI = os.environ.get("ALCHEMICAL_DATABASE_URI")
|
||||||
|
if ALCHEMICAL_DATABASE_URI is None:
|
||||||
|
raise ValueError("ALCHEMICAL_DATABASE_URI is undefined")
|
||||||
|
if "unittest" in sys.modules and "sqlite" not in ALCHEMICAL_DATABASE_URI:
|
||||||
|
raise ValueError("Unit tests running on non-Sqlite database")
|
||||||
|
db = Alchemical(ALCHEMICAL_DATABASE_URI)
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
class Carts(Base):
|
class Carts(dbtables.CartsTable):
|
||||||
__tablename__ = "carts"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
||||||
cart_number: Mapped[int] = mapped_column(unique=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(256), index=True)
|
|
||||||
duration: Mapped[Optional[int]] = mapped_column(index=True)
|
|
||||||
path: Mapped[Optional[str]] = mapped_column(String(2048), index=False)
|
|
||||||
enabled: Mapped[Optional[bool]] = mapped_column(default=False)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<Carts(id={self.id}, cart={self.cart_number}, "
|
|
||||||
f"name={self.name}, path={self.path}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: Session,
|
||||||
cart_number: int,
|
cart_number: int,
|
||||||
name: str,
|
name: str,
|
||||||
duration: Optional[int] = None,
|
duration: Optional[int] = None,
|
||||||
@ -81,26 +59,10 @@ class Carts(Base):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
class NoteColours(Base):
|
class NoteColours(dbtables.NoteColoursTable):
|
||||||
__tablename__ = "notecolours"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
||||||
substring: Mapped[str] = mapped_column(String(256), index=False)
|
|
||||||
colour: Mapped[str] = mapped_column(String(21), index=False)
|
|
||||||
enabled: Mapped[bool] = mapped_column(default=True, index=True)
|
|
||||||
is_regex: Mapped[bool] = mapped_column(default=False, index=False)
|
|
||||||
is_casesensitive: Mapped[bool] = mapped_column(default=False, index=False)
|
|
||||||
order: Mapped[Optional[int]] = mapped_column(index=True)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<NoteColour(id={self.id}, substring={self.substring}, "
|
|
||||||
f"colour={self.colour}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: Session,
|
||||||
substring: str,
|
substring: str,
|
||||||
colour: str,
|
colour: str,
|
||||||
enabled: bool = True,
|
enabled: bool = True,
|
||||||
@ -116,10 +78,10 @@ class NoteColours(Base):
|
|||||||
self.order = order
|
self.order = order
|
||||||
|
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls, session: scoped_session) -> Sequence["NoteColours"]:
|
def get_all(cls, session: Session) -> Sequence["NoteColours"]:
|
||||||
"""
|
"""
|
||||||
Return all records
|
Return all records
|
||||||
"""
|
"""
|
||||||
@ -127,7 +89,7 @@ class NoteColours(Base):
|
|||||||
return session.scalars(select(cls)).all()
|
return session.scalars(select(cls)).all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_colour(session: scoped_session, text: str) -> Optional[str]:
|
def get_colour(session: Session, text: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Parse text and return colour string if matched, else empty string
|
Parse text and return colour string if matched, else empty string
|
||||||
"""
|
"""
|
||||||
@ -158,30 +120,17 @@ class NoteColours(Base):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Playdates(Base):
|
class Playdates(dbtables.PlaydatesTable):
|
||||||
__tablename__ = "playdates"
|
def __init__(self, session: Session, track_id: int) -> None:
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
||||||
lastplayed: Mapped[datetime] = mapped_column(index=True)
|
|
||||||
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id"))
|
|
||||||
track: Mapped["Tracks"] = relationship("Tracks", back_populates="playdates")
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<Playdates(id={self.id}, track_id={self.track_id} "
|
|
||||||
f"lastplayed={self.lastplayed}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, session: scoped_session, track_id: int) -> None:
|
|
||||||
"""Record that track was played"""
|
"""Record that track was played"""
|
||||||
|
|
||||||
self.lastplayed = datetime.now()
|
self.lastplayed = dt.datetime.now()
|
||||||
self.track_id = track_id
|
self.track_id = track_id
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def last_played(session: scoped_session, track_id: int) -> datetime:
|
def last_played(session: Session, track_id: int) -> dt.datetime:
|
||||||
"""Return datetime track last played or None"""
|
"""Return datetime track last played or None"""
|
||||||
|
|
||||||
last_played = session.execute(
|
last_played = session.execute(
|
||||||
@ -194,10 +143,12 @@ class Playdates(Base):
|
|||||||
if last_played:
|
if last_played:
|
||||||
return last_played[0]
|
return last_played[0]
|
||||||
else:
|
else:
|
||||||
return Config.EPOCH
|
# Should never be reached as we create record with a
|
||||||
|
# last_played value
|
||||||
|
return Config.EPOCH # pragma: no cover
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def played_after(session: scoped_session, since: datetime) -> Sequence["Playdates"]:
|
def played_after(session: Session, since: dt.datetime) -> Sequence["Playdates"]:
|
||||||
"""Return a list of Playdates objects since passed time"""
|
"""Return a list of Playdates objects since passed time"""
|
||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
@ -207,50 +158,20 @@ class Playdates(Base):
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
|
||||||
class Playlists(Base):
|
class Playlists(dbtables.PlaylistsTable):
|
||||||
"""
|
def __init__(self, session: Session, name: str):
|
||||||
Manage playlists
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "playlists"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(32), unique=True)
|
|
||||||
last_used: Mapped[Optional[datetime]] = mapped_column(DateTime, default=None)
|
|
||||||
tab: Mapped[Optional[int]] = mapped_column(default=None)
|
|
||||||
open: Mapped[bool] = mapped_column(default=False)
|
|
||||||
is_template: Mapped[bool] = mapped_column(default=False)
|
|
||||||
deleted: Mapped[bool] = mapped_column(default=False)
|
|
||||||
rows: Mapped[List["PlaylistRows"]] = relationship(
|
|
||||||
"PlaylistRows",
|
|
||||||
back_populates="playlist",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
order_by="PlaylistRows.plr_rownum",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<Playlists(id={self.id}, name={self.name}, "
|
|
||||||
f"is_templatee={self.is_template}, open={self.open}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, session: scoped_session, name: str):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clear_tabs(session: scoped_session, playlist_ids: List[int]) -> None:
|
def clear_tabs(session: Session, playlist_ids: List[int]) -> None:
|
||||||
"""
|
"""
|
||||||
Make all tab records NULL
|
Make all tab records NULL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
session.execute(
|
session.execute(
|
||||||
update(Playlists)
|
update(Playlists).where((Playlists.id.in_(playlist_ids))).values(tab=None)
|
||||||
.where(
|
|
||||||
(Playlists.id.in_(playlist_ids))
|
|
||||||
)
|
|
||||||
.values(tab=None)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
@ -260,7 +181,7 @@ class Playlists(Base):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_playlist_from_template(
|
def create_playlist_from_template(
|
||||||
cls, session: scoped_session, template: "Playlists", playlist_name: str
|
cls, session: Session, template: "Playlists", playlist_name: str
|
||||||
) -> Optional["Playlists"]:
|
) -> Optional["Playlists"]:
|
||||||
"""Create a new playlist from template"""
|
"""Create a new playlist from template"""
|
||||||
|
|
||||||
@ -274,16 +195,16 @@ class Playlists(Base):
|
|||||||
|
|
||||||
return playlist
|
return playlist
|
||||||
|
|
||||||
def delete(self, session: scoped_session) -> None:
|
def delete(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
Mark as deleted
|
Mark as deleted
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.deleted = True
|
self.deleted = True
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls, session: scoped_session) -> Sequence["Playlists"]:
|
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all playlists ordered by last use"""
|
"""Returns a list of all playlists ordered by last use"""
|
||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
@ -293,7 +214,7 @@ class Playlists(Base):
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_templates(cls, session: scoped_session) -> Sequence["Playlists"]:
|
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all templates ordered by name"""
|
"""Returns a list of all templates ordered by name"""
|
||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
@ -301,7 +222,7 @@ class Playlists(Base):
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_closed(cls, session: scoped_session) -> Sequence["Playlists"]:
|
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all closed playlists ordered by last use"""
|
"""Returns a list of all closed playlists ordered by last use"""
|
||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
@ -315,7 +236,7 @@ class Playlists(Base):
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_open(cls, session: scoped_session) -> Sequence[Optional["Playlists"]]:
|
def get_open(cls, session: Session) -> Sequence[Optional["Playlists"]]:
|
||||||
"""
|
"""
|
||||||
Return a list of loaded playlists ordered by tab.
|
Return a list of loaded playlists ordered by tab.
|
||||||
"""
|
"""
|
||||||
@ -328,10 +249,10 @@ class Playlists(Base):
|
|||||||
"""Mark playlist as loaded and used now"""
|
"""Mark playlist as loaded and used now"""
|
||||||
|
|
||||||
self.open = True
|
self.open = True
|
||||||
self.last_used = datetime.now()
|
self.last_used = dt.datetime.now()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name_is_available(session: scoped_session, name: str) -> bool:
|
def name_is_available(session: Session, name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if no playlist of this name exists else false.
|
Return True if no playlist of this name exists else false.
|
||||||
"""
|
"""
|
||||||
@ -341,17 +262,17 @@ class Playlists(Base):
|
|||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
|
|
||||||
def rename(self, session: scoped_session, new_name: str) -> None:
|
def rename(self, session: Session, new_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Rename playlist
|
Rename playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.name = new_name
|
self.name = new_name
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_as_template(
|
def save_as_template(
|
||||||
session: scoped_session, playlist_id: int, template_name: str
|
session: Session, playlist_id: int, template_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Save passed playlist as new template"""
|
"""Save passed playlist as new template"""
|
||||||
|
|
||||||
@ -365,35 +286,10 @@ class Playlists(Base):
|
|||||||
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
||||||
|
|
||||||
|
|
||||||
class PlaylistRows(Base):
|
class PlaylistRows(dbtables.PlaylistRowsTable):
|
||||||
__tablename__ = "playlist_rows"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
||||||
plr_rownum: Mapped[int]
|
|
||||||
note: Mapped[str] = mapped_column(
|
|
||||||
String(2048), index=False, default="", nullable=False
|
|
||||||
)
|
|
||||||
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"))
|
|
||||||
playlist: Mapped[Playlists] = relationship(back_populates="rows")
|
|
||||||
track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id"))
|
|
||||||
track: Mapped["Tracks"] = relationship(
|
|
||||||
"Tracks",
|
|
||||||
back_populates="playlistrows",
|
|
||||||
)
|
|
||||||
played: Mapped[bool] = mapped_column(
|
|
||||||
Boolean, nullable=False, index=False, default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<PlaylistRow(id={self.id}, playlist_id={self.playlist_id}, "
|
|
||||||
f"track_id={self.track_id}, "
|
|
||||||
f"note={self.note}, plr_rownum={self.plr_rownum}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: Session,
|
||||||
playlist_id: int,
|
playlist_id: int,
|
||||||
row_number: int,
|
row_number: int,
|
||||||
note: str = "",
|
note: str = "",
|
||||||
@ -406,7 +302,7 @@ class PlaylistRows(Base):
|
|||||||
self.plr_rownum = row_number
|
self.plr_rownum = row_number
|
||||||
self.note = note
|
self.note = note
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
def append_note(self, extra_note: str) -> None:
|
def append_note(self, extra_note: str) -> None:
|
||||||
"""Append passed note to any existing note"""
|
"""Append passed note to any existing note"""
|
||||||
@ -418,7 +314,7 @@ class PlaylistRows(Base):
|
|||||||
self.note = extra_note
|
self.note = extra_note
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None:
|
def copy_playlist(session: Session, src_id: int, dst_id: int) -> None:
|
||||||
"""Copy playlist entries"""
|
"""Copy playlist entries"""
|
||||||
|
|
||||||
src_rows = session.scalars(
|
src_rows = session.scalars(
|
||||||
@ -436,7 +332,7 @@ class PlaylistRows(Base):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def deep_row(
|
def deep_row(
|
||||||
cls, session: scoped_session, playlist_id: int, row_number: int
|
cls, session: Session, playlist_id: int, row_number: int
|
||||||
) -> "PlaylistRows":
|
) -> "PlaylistRows":
|
||||||
"""
|
"""
|
||||||
Return a playlist row that includes full track and lastplayed data for
|
Return a playlist row that includes full track and lastplayed data for
|
||||||
@ -456,9 +352,7 @@ class PlaylistRows(Base):
|
|||||||
return session.execute(stmt).unique().scalar_one()
|
return session.execute(stmt).unique().scalar_one()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def deep_rows(
|
def deep_rows(cls, session: Session, playlist_id: int) -> Sequence["PlaylistRows"]:
|
||||||
cls, session: scoped_session, playlist_id: int
|
|
||||||
) -> Sequence["PlaylistRows"]:
|
|
||||||
"""
|
"""
|
||||||
Return a list of playlist rows that include full track and lastplayed data for
|
Return a list of playlist rows that include full track and lastplayed data for
|
||||||
given playlist_id., Sequence
|
given playlist_id., Sequence
|
||||||
@ -475,9 +369,7 @@ class PlaylistRows(Base):
|
|||||||
return session.scalars(stmt).unique().all()
|
return session.scalars(stmt).unique().all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_higher_rows(
|
def delete_higher_rows(session: Session, playlist_id: int, maxrow: int) -> None:
|
||||||
session: scoped_session, playlist_id: int, maxrow: int
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Delete rows in given playlist that have a higher row number
|
Delete rows in given playlist that have a higher row number
|
||||||
than 'maxrow'
|
than 'maxrow'
|
||||||
@ -489,10 +381,10 @@ class PlaylistRows(Base):
|
|||||||
PlaylistRows.plr_rownum > maxrow,
|
PlaylistRows.plr_rownum > maxrow,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_row(session: scoped_session, playlist_id: int, row_number: int) -> None:
|
def delete_row(session: Session, playlist_id: int, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Delete passed row in given playlist.
|
Delete passed row in given playlist.
|
||||||
"""
|
"""
|
||||||
@ -505,7 +397,7 @@ class PlaylistRows(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fixup_rownumbers(session: scoped_session, playlist_id: int) -> None:
|
def fixup_rownumbers(session: Session, playlist_id: int) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure the row numbers for passed playlist have no gaps
|
Ensure the row numbers for passed playlist have no gaps
|
||||||
"""
|
"""
|
||||||
@ -524,7 +416,7 @@ class PlaylistRows(Base):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def plrids_to_plrs(
|
def plrids_to_plrs(
|
||||||
cls, session: scoped_session, playlist_id: int, plr_ids: List[int]
|
cls, session: Session, playlist_id: int, plr_ids: List[int]
|
||||||
) -> Sequence["PlaylistRows"]:
|
) -> Sequence["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
Take a list of PlaylistRows ids and return a list of corresponding
|
Take a list of PlaylistRows ids and return a list of corresponding
|
||||||
@ -540,7 +432,7 @@ class PlaylistRows(Base):
|
|||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]:
|
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 the last used row for playlist, or None if no rows"""
|
||||||
|
|
||||||
return session.execute(
|
return session.execute(
|
||||||
@ -551,7 +443,7 @@ class PlaylistRows(Base):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_track_plr(
|
def get_track_plr(
|
||||||
session: scoped_session, track_id: int, playlist_id: int
|
session: Session, track_id: int, playlist_id: int
|
||||||
) -> Optional["PlaylistRows"]:
|
) -> Optional["PlaylistRows"]:
|
||||||
"""Return first matching PlaylistRows object or None"""
|
"""Return first matching PlaylistRows object or None"""
|
||||||
|
|
||||||
@ -566,7 +458,7 @@ class PlaylistRows(Base):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_played_rows(
|
def get_played_rows(
|
||||||
cls, session: scoped_session, playlist_id: int
|
cls, session: Session, playlist_id: int
|
||||||
) -> Sequence["PlaylistRows"]:
|
) -> Sequence["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
For passed playlist, return a list of rows that
|
For passed playlist, return a list of rows that
|
||||||
@ -584,10 +476,8 @@ class PlaylistRows(Base):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_rows_with_tracks(
|
def get_rows_with_tracks(
|
||||||
cls,
|
cls,
|
||||||
session: scoped_session,
|
session: Session,
|
||||||
playlist_id: int,
|
playlist_id: int,
|
||||||
from_row: Optional[int] = None,
|
|
||||||
to_row: Optional[int] = None,
|
|
||||||
) -> Sequence["PlaylistRows"]:
|
) -> Sequence["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
For passed playlist, return a list of rows that
|
For passed playlist, return a list of rows that
|
||||||
@ -597,18 +487,13 @@ class PlaylistRows(Base):
|
|||||||
query = select(cls).where(
|
query = select(cls).where(
|
||||||
cls.playlist_id == playlist_id, cls.track_id.is_not(None)
|
cls.playlist_id == playlist_id, cls.track_id.is_not(None)
|
||||||
)
|
)
|
||||||
if from_row is not None:
|
|
||||||
query = query.where(cls.plr_rownum >= from_row)
|
|
||||||
if to_row is not None:
|
|
||||||
query = query.where(cls.plr_rownum <= to_row)
|
|
||||||
|
|
||||||
plrs = session.scalars((query).order_by(cls.plr_rownum)).all()
|
plrs = session.scalars((query).order_by(cls.plr_rownum)).all()
|
||||||
|
|
||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_unplayed_rows(
|
def get_unplayed_rows(
|
||||||
cls, session: scoped_session, playlist_id: int
|
cls, session: Session, playlist_id: int
|
||||||
) -> Sequence["PlaylistRows"]:
|
) -> Sequence["PlaylistRows"]:
|
||||||
"""
|
"""
|
||||||
For passed playlist, return a list of playlist rows that
|
For passed playlist, return a list of playlist rows that
|
||||||
@ -629,14 +514,25 @@ class PlaylistRows(Base):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def insert_row(
|
def insert_row(
|
||||||
cls, session: scoped_session, playlist_id: int, new_row_number: int
|
cls,
|
||||||
|
session: Session,
|
||||||
|
playlist_id: int,
|
||||||
|
new_row_number: int,
|
||||||
|
note: str = "",
|
||||||
|
track_id: Optional[int] = None,
|
||||||
) -> "PlaylistRows":
|
) -> "PlaylistRows":
|
||||||
cls.move_rows_down(session, playlist_id, new_row_number, 1)
|
cls.move_rows_down(session, playlist_id, new_row_number, 1)
|
||||||
return cls(session, playlist_id, new_row_number)
|
return cls(
|
||||||
|
session,
|
||||||
|
playlist_id=playlist_id,
|
||||||
|
row_number=new_row_number,
|
||||||
|
note=note,
|
||||||
|
track_id=track_id,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def move_rows_down(
|
def move_rows_down(
|
||||||
session: scoped_session, playlist_id: int, starting_row: int, move_by: int
|
session: Session, playlist_id: int, starting_row: int, move_by: int
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Create space to insert move_by additional rows by incremented row
|
Create space to insert move_by additional rows by incremented row
|
||||||
@ -656,7 +552,7 @@ class PlaylistRows(Base):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_plr_rownumbers(
|
def update_plr_rownumbers(
|
||||||
session: scoped_session, playlist_id: int, sqla_map: List[dict[str, int]]
|
session: Session, playlist_id: int, sqla_map: List[dict[str, int]]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Take a {plrid: plr_rownum} dictionary and update the row numbers accordingly
|
Take a {plrid: plr_rownum} dictionary and update the row numbers accordingly
|
||||||
@ -675,27 +571,11 @@ class PlaylistRows(Base):
|
|||||||
session.connection().execute(stmt, sqla_map)
|
session.connection().execute(stmt, sqla_map)
|
||||||
|
|
||||||
|
|
||||||
class Settings(Base):
|
class Settings(dbtables.SettingsTable):
|
||||||
"""Manage settings"""
|
def __init__(self, session: Session, name: str):
|
||||||
|
|
||||||
__tablename__ = "settings"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
||||||
name: Mapped[str] = mapped_column(String(64), unique=True)
|
|
||||||
f_datetime: Mapped[Optional[datetime]] = mapped_column(default=None)
|
|
||||||
f_int: Mapped[Optional[int]] = mapped_column(default=None)
|
|
||||||
f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<Settings(id={self.id}, name={self.name}, "
|
|
||||||
f"f_datetime={self.f_datetime}, f_int={self.f_int}, f_string={self.f_string}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, session: scoped_session, name: str):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all_as_dict(cls, session):
|
def all_as_dict(cls, session):
|
||||||
@ -712,7 +592,7 @@ class Settings(Base):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_int_settings(cls, session: scoped_session, name: str) -> "Settings":
|
def get_int_settings(cls, session: Session, name: str) -> "Settings":
|
||||||
"""Get setting for an integer or return new setting record"""
|
"""Get setting for an integer or return new setting record"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -721,45 +601,17 @@ class Settings(Base):
|
|||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
return Settings(session, name)
|
return Settings(session, name)
|
||||||
|
|
||||||
def update(self, session: scoped_session, data: dict) -> None:
|
def update(self, session: Session, data: dict) -> None:
|
||||||
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)
|
||||||
session.flush()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
class Tracks(Base):
|
class Tracks(dbtables.TracksTable):
|
||||||
__tablename__ = "tracks"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
||||||
title: Mapped[str] = mapped_column(String(256), index=True)
|
|
||||||
artist: Mapped[str] = mapped_column(String(256), index=True)
|
|
||||||
bitrate: Mapped[Optional[int]] = mapped_column(default=None)
|
|
||||||
duration: Mapped[int] = mapped_column(index=True)
|
|
||||||
fade_at: Mapped[int] = mapped_column(index=False)
|
|
||||||
mtime: Mapped[float] = mapped_column(index=True)
|
|
||||||
path: Mapped[str] = mapped_column(String(2048), index=False, unique=True)
|
|
||||||
silence_at: Mapped[int] = mapped_column(index=False)
|
|
||||||
start_gap: Mapped[int] = mapped_column(index=False)
|
|
||||||
playlistrows: Mapped[List[PlaylistRows]] = relationship(
|
|
||||||
"PlaylistRows", back_populates="track"
|
|
||||||
)
|
|
||||||
playlists = association_proxy("playlistrows", "playlist")
|
|
||||||
playdates: Mapped[List[Playdates]] = relationship(
|
|
||||||
"Playdates",
|
|
||||||
back_populates="track",
|
|
||||||
lazy="joined",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"<Track(id={self.id}, title={self.title}, "
|
|
||||||
f"artist={self.artist}, path={self.path}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: scoped_session,
|
session: Session,
|
||||||
path: str,
|
path: str,
|
||||||
title: str,
|
title: str,
|
||||||
artist: str,
|
artist: str,
|
||||||
@ -795,7 +647,7 @@ class Tracks(Base):
|
|||||||
return session.scalars(select(cls)).unique().all()
|
return session.scalars(select(cls)).unique().all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_path(cls, session: scoped_session, path: str) -> Optional["Tracks"]:
|
def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]:
|
||||||
"""
|
"""
|
||||||
Return track with passed path, or None.
|
Return track with passed path, or None.
|
||||||
"""
|
"""
|
||||||
@ -810,7 +662,7 @@ class Tracks(Base):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_artists(cls, session: scoped_session, text: str) -> Sequence["Tracks"]:
|
def search_artists(cls, session: Session, text: str) -> Sequence["Tracks"]:
|
||||||
"""
|
"""
|
||||||
Search case-insenstively for artists containing str
|
Search case-insenstively for artists containing str
|
||||||
|
|
||||||
@ -831,7 +683,7 @@ class Tracks(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_titles(cls, session: scoped_session, text: str) -> Sequence["Tracks"]:
|
def search_titles(cls, session: Session, text: str) -> Sequence["Tracks"]:
|
||||||
"""
|
"""
|
||||||
Search case-insenstively for titles containing str
|
Search case-insenstively for titles containing str
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
# Standard library imports
|
||||||
from time import sleep
|
|
||||||
from typing import (
|
|
||||||
cast,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
)
|
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
|
from time import sleep
|
||||||
|
from typing import cast, List, Optional
|
||||||
import argparse
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import pipeclient
|
# PyQt imports
|
||||||
from pygame import mixer
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
QDate,
|
QDate,
|
||||||
@ -49,8 +44,14 @@ from PyQt6.QtWidgets import (
|
|||||||
QProgressBar,
|
QProgressBar,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from pygame import mixer
|
||||||
|
import pipeclient
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
import stackprinter # type: ignore
|
import stackprinter # type: ignore
|
||||||
|
|
||||||
|
# App imports
|
||||||
from classes import (
|
from classes import (
|
||||||
track_sequence,
|
track_sequence,
|
||||||
FadeCurve,
|
FadeCurve,
|
||||||
@ -58,23 +59,18 @@ from classes import (
|
|||||||
PlaylistTrack,
|
PlaylistTrack,
|
||||||
)
|
)
|
||||||
from config import Config
|
from config import Config
|
||||||
from dbconfig import (
|
|
||||||
engine,
|
|
||||||
scoped_session,
|
|
||||||
Session,
|
|
||||||
)
|
|
||||||
from dialogs import TrackSelectDialog
|
from dialogs import TrackSelectDialog
|
||||||
from log import log
|
from log import log
|
||||||
from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
from models import db, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
||||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||||
from playlists import PlaylistTab
|
from playlists import PlaylistTab
|
||||||
|
from ui import icons_rc # noqa F401
|
||||||
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
|
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
|
||||||
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
||||||
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
||||||
from ui.main_window_ui import Ui_MainWindow # type: ignore
|
from ui.main_window_ui import Ui_MainWindow # type: ignore
|
||||||
from utilities import check_db, update_bitrates
|
from utilities import check_db, update_bitrates
|
||||||
import helpers
|
import helpers
|
||||||
from ui import icons_rc # noqa F401
|
|
||||||
import music
|
import music
|
||||||
|
|
||||||
|
|
||||||
@ -168,7 +164,7 @@ class ImportTrack(QObject):
|
|||||||
Create track objects from passed files and add to visible playlist
|
Create track objects from passed files and add to visible playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
for fname in self.filenames:
|
for fname in self.filenames:
|
||||||
self.signals.status_message_signal.emit(
|
self.signals.status_message_signal.emit(
|
||||||
f"Importing {basename(fname)}", 5000
|
f"Importing {basename(fname)}", 5000
|
||||||
@ -208,14 +204,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.music: music.Music = music.Music()
|
self.music: music.Music = music.Music()
|
||||||
self.playing: bool = False
|
self.playing: bool = False
|
||||||
|
|
||||||
self.selected_plrs: Optional[List[PlaylistRows]] = None
|
|
||||||
|
|
||||||
self.set_main_window_size()
|
self.set_main_window_size()
|
||||||
self.lblSumPlaytime = QLabel("")
|
self.lblSumPlaytime = QLabel("")
|
||||||
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
|
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
|
||||||
self.txtSearch = QLineEdit()
|
self.txtSearch = QLineEdit()
|
||||||
self.statusbar.addWidget(self.txtSearch)
|
|
||||||
self.txtSearch.setHidden(True)
|
self.txtSearch.setHidden(True)
|
||||||
|
self.statusbar.addWidget(self.txtSearch)
|
||||||
self.hide_played_tracks = False
|
self.hide_played_tracks = False
|
||||||
mixer.init()
|
mixer.init()
|
||||||
self.widgetFadeVolume.hideAxis("bottom")
|
self.widgetFadeVolume.hideAxis("bottom")
|
||||||
@ -257,7 +251,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
except subprocess.CalledProcessError as exc_info:
|
except subprocess.CalledProcessError as exc_info:
|
||||||
git_tag = str(exc_info.output)
|
git_tag = str(exc_info.output)
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
if session.bind:
|
if session.bind:
|
||||||
dbname = session.bind.engine.url.database
|
dbname = session.bind.engine.url.database
|
||||||
|
|
||||||
@ -319,7 +313,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def cart_edit(self, btn: CartButton, event: QEvent):
|
def cart_edit(self, btn: CartButton, event: QEvent):
|
||||||
"""Handle context menu for cart button"""
|
"""Handle context menu for cart button"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
cart = session.query(Carts).get(btn.cart_id)
|
cart = session.query(Carts).get(btn.cart_id)
|
||||||
if cart is None:
|
if cart is None:
|
||||||
log.error("cart_edit: cart not found")
|
log.error("cart_edit: cart not found")
|
||||||
@ -351,7 +345,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def carts_init(self) -> None:
|
def carts_init(self) -> None:
|
||||||
"""Initialse carts data structures"""
|
"""Initialse carts data structures"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
# Number carts from 1 for humanity
|
# Number carts from 1 for humanity
|
||||||
for cart_number in range(1, Config.CARTS_COUNT + 1):
|
for cart_number in range(1, Config.CARTS_COUNT + 1):
|
||||||
cart = session.query(Carts).get(cart_number)
|
cart = session.query(Carts).get(cart_number)
|
||||||
@ -428,7 +422,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self, "Track playing", "Can't close application while track is playing"
|
self, "Track playing", "Can't close application while track is playing"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
settings = Settings.all_as_dict(session)
|
settings = Settings.all_as_dict(session)
|
||||||
record = settings["mainwindow_height"]
|
record = settings["mainwindow_height"]
|
||||||
if record.f_int != self.height():
|
if record.f_int != self.height():
|
||||||
@ -497,7 +491,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Record playlist as closed and update remaining playlist tabs
|
# Record playlist as closed and update remaining playlist tabs
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
playlist = session.get(Playlists, closing_tab_playlist_id)
|
playlist = session.get(Playlists, closing_tab_playlist_id)
|
||||||
if playlist:
|
if playlist:
|
||||||
playlist.close()
|
playlist.close()
|
||||||
@ -568,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: scoped_session, playlist_name: Optional[str] = None
|
self, session: Session, playlist_name: Optional[str] = None
|
||||||
) -> Optional[Playlists]:
|
) -> Optional[Playlists]:
|
||||||
"""Create new playlist"""
|
"""Create new playlist"""
|
||||||
|
|
||||||
@ -590,7 +584,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def create_and_show_playlist(self) -> None:
|
def create_and_show_playlist(self) -> None:
|
||||||
"""Create new playlist and display it"""
|
"""Create new playlist and display it"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
playlist = self.create_playlist(session)
|
playlist = self.create_playlist(session)
|
||||||
if playlist:
|
if playlist:
|
||||||
self.create_playlist_tab(playlist)
|
self.create_playlist_tab(playlist)
|
||||||
@ -638,7 +632,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Delete current playlist
|
Delete current playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
playlist_id = self.active_tab().playlist_id
|
playlist_id = self.active_tab().playlist_id
|
||||||
playlist = session.get(Playlists, playlist_id)
|
playlist = session.get(Playlists, playlist_id)
|
||||||
if playlist:
|
if playlist:
|
||||||
@ -672,7 +666,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
path += ".csv"
|
path += ".csv"
|
||||||
|
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
for playdate in Playdates.played_after(session, start_dt):
|
for playdate in Playdates.played_after(session, start_dt):
|
||||||
f.write(f"{playdate.track.artist},{playdate.track.title}\n")
|
f.write(f"{playdate.track.artist},{playdate.track.title}\n")
|
||||||
|
|
||||||
@ -704,7 +698,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
playlist_id = self.active_tab().playlist_id
|
playlist_id = self.active_tab().playlist_id
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
# Get output filename
|
# Get output filename
|
||||||
playlist = session.get(Playlists, playlist_id)
|
playlist = session.get(Playlists, playlist_id)
|
||||||
if not playlist:
|
if not playlist:
|
||||||
@ -755,7 +749,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if track_sequence.now.track_id is None or track_sequence.now.start_time is None:
|
if track_sequence.now.track_id is None or track_sequence.now.start_time is None:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
now = datetime.now()
|
now = dt.datetime.now()
|
||||||
track_start = track_sequence.now.start_time
|
track_start = track_sequence.now.start_time
|
||||||
elapsed_seconds = (now - track_start).total_seconds()
|
elapsed_seconds = (now - track_start).total_seconds()
|
||||||
return int(elapsed_seconds * 1000)
|
return int(elapsed_seconds * 1000)
|
||||||
@ -786,7 +780,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if not dlg.exec():
|
if not dlg.exec():
|
||||||
return
|
return
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
new_tracks = []
|
new_tracks = []
|
||||||
for fname in dlg.selectedFiles():
|
for fname in dlg.selectedFiles():
|
||||||
txt = ""
|
txt = ""
|
||||||
@ -885,7 +879,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.active_tab().source_model_selected_row_number()
|
self.active_tab().source_model_selected_row_number()
|
||||||
or self.active_proxy_model().rowCount()
|
or self.active_proxy_model().rowCount()
|
||||||
)
|
)
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
dlg = TrackSelectDialog(
|
dlg = TrackSelectDialog(
|
||||||
session=session,
|
session=session,
|
||||||
new_row_number=new_row_number,
|
new_row_number=new_row_number,
|
||||||
@ -897,12 +891,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
"""Load the playlists that were open when the last session closed"""
|
"""Load the playlists that were open when the last session closed"""
|
||||||
|
|
||||||
playlist_ids = []
|
playlist_ids = []
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
for playlist in Playlists.get_open(session):
|
for playlist in Playlists.get_open(session):
|
||||||
if playlist:
|
if playlist:
|
||||||
_ = self.create_playlist_tab(playlist)
|
_ = self.create_playlist_tab(playlist)
|
||||||
playlist_ids.append(playlist.id)
|
playlist_ids.append(playlist.id)
|
||||||
log.info(f"load_last_playlists() loaded {playlist=}")
|
log.debug(f"load_last_playlists() loaded {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.f_int is not None and record.f_int >= 0:
|
if record.f_int is not None and record.f_int >= 0:
|
||||||
@ -954,7 +948,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
visible_tab = self.active_tab()
|
visible_tab = self.active_tab()
|
||||||
source_playlist_id = visible_tab.playlist_id
|
source_playlist_id = visible_tab.playlist_id
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
for playlist in Playlists.get_all(session):
|
for playlist in Playlists.get_all(session):
|
||||||
if playlist.id == source_playlist_id:
|
if playlist.id == source_playlist_id:
|
||||||
continue
|
continue
|
||||||
@ -1007,7 +1001,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def new_from_template(self) -> None:
|
def new_from_template(self) -> None:
|
||||||
"""Create new playlist from template"""
|
"""Create new playlist from template"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
templates = Playlists.get_all_templates(session)
|
templates = Playlists.get_all_templates(session)
|
||||||
dlg = SelectPlaylistDialog(self, playlists=templates, session=session)
|
dlg = SelectPlaylistDialog(self, playlists=templates, session=session)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
@ -1033,7 +1027,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def open_playlist(self) -> None:
|
def open_playlist(self) -> None:
|
||||||
"""Open existing playlist"""
|
"""Open existing playlist"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
playlists = Playlists.get_closed(session)
|
playlists = Playlists.get_closed(session)
|
||||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
@ -1199,7 +1193,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Rename current playlist
|
Rename current playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
playlist_id = self.active_tab().playlist_id
|
playlist_id = self.active_tab().playlist_id
|
||||||
playlist = session.get(Playlists, playlist_id)
|
playlist = session.get(Playlists, playlist_id)
|
||||||
if playlist:
|
if playlist:
|
||||||
@ -1243,12 +1237,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
and track_sequence.now.resume_marker
|
and track_sequence.now.resume_marker
|
||||||
):
|
):
|
||||||
elapsed_ms = track_sequence.now.duration * track_sequence.now.resume_marker
|
elapsed_ms = track_sequence.now.duration * track_sequence.now.resume_marker
|
||||||
track_sequence.now.start_time -= timedelta(milliseconds=elapsed_ms)
|
track_sequence.now.start_time -= dt.timedelta(milliseconds=elapsed_ms)
|
||||||
|
|
||||||
def save_as_template(self) -> None:
|
def save_as_template(self) -> None:
|
||||||
"""Save current playlist as template"""
|
"""Save current playlist as template"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
template_names = [a.name for a in Playlists.get_all_templates(session)]
|
template_names = [a.name for a in Playlists.get_all_templates(session)]
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@ -1310,21 +1304,39 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def set_main_window_size(self) -> None:
|
def set_main_window_size(self) -> None:
|
||||||
"""Set size of window from database"""
|
"""Set size of window from database"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
settings = Settings.all_as_dict(session)
|
settings = Settings.all_as_dict(session)
|
||||||
|
if "mainwindow_x" in settings:
|
||||||
record = settings["mainwindow_x"]
|
record = settings["mainwindow_x"]
|
||||||
x = record.f_int or 1
|
x = record.f_int or 1
|
||||||
|
else:
|
||||||
|
x = 100
|
||||||
|
if "mainwindow_y" in settings:
|
||||||
record = settings["mainwindow_y"]
|
record = settings["mainwindow_y"]
|
||||||
y = record.f_int or 1
|
y = record.f_int or 1
|
||||||
|
else:
|
||||||
|
y = 100
|
||||||
|
if "mainwindow_width" in settings:
|
||||||
record = settings["mainwindow_width"]
|
record = settings["mainwindow_width"]
|
||||||
width = record.f_int or 1599
|
width = record.f_int or 1599
|
||||||
|
else:
|
||||||
|
width = 100
|
||||||
|
if "mainwindow_height" in settings:
|
||||||
record = settings["mainwindow_height"]
|
record = settings["mainwindow_height"]
|
||||||
height = record.f_int or 981
|
height = record.f_int or 981
|
||||||
|
else:
|
||||||
|
height = 100
|
||||||
self.setGeometry(x, y, width, height)
|
self.setGeometry(x, y, width, height)
|
||||||
|
if "splitter_top" in settings:
|
||||||
record = settings["splitter_top"]
|
record = settings["splitter_top"]
|
||||||
splitter_top = record.f_int or 256
|
splitter_top = record.f_int or 256
|
||||||
|
else:
|
||||||
|
splitter_top = 100
|
||||||
|
if "splitter_bottom" in settings:
|
||||||
record = settings["splitter_bottom"]
|
record = settings["splitter_bottom"]
|
||||||
splitter_bottom = record.f_int or 256
|
splitter_bottom = record.f_int or 256
|
||||||
|
else:
|
||||||
|
splitter_bottom = 100
|
||||||
self.splitter.setSizes([splitter_top, splitter_bottom])
|
self.splitter.setSizes([splitter_top, splitter_bottom])
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1397,7 +1409,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: scoped_session, default: str = ""
|
self, session: Session, default: str = ""
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Get name of new playlist from user"""
|
"""Get name of new playlist from user"""
|
||||||
|
|
||||||
@ -1498,13 +1510,19 @@ 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
|
||||||
and track_sequence.now.start_time
|
and track_sequence.now.start_time
|
||||||
):
|
):
|
||||||
play_time = (
|
play_time = (
|
||||||
datetime.now() - track_sequence.now.start_time
|
dt.datetime.now() - track_sequence.now.start_time
|
||||||
).total_seconds() * 1000
|
).total_seconds() * 1000
|
||||||
track_sequence.now.fade_graph.tick(play_time)
|
track_sequence.now.fade_graph.tick(play_time)
|
||||||
|
|
||||||
@ -1513,7 +1531,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Called every 500ms
|
Called every 500ms
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
|
self.lblTOD.setText(dt.datetime.now().strftime(Config.TOD_TIME_FORMAT))
|
||||||
# Update carts
|
# Update carts
|
||||||
# self.cart_tick()
|
# self.cart_tick()
|
||||||
|
|
||||||
@ -1541,8 +1559,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
and track_sequence.now.start_time
|
and track_sequence.now.start_time
|
||||||
and (
|
and (
|
||||||
self.music.player.is_playing()
|
self.music.player.is_playing()
|
||||||
or (datetime.now() - track_sequence.now.start_time)
|
or (dt.datetime.now() - track_sequence.now.start_time)
|
||||||
< timedelta(microseconds=Config.PLAY_SETTLE)
|
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
playtime = self.get_playtime()
|
playtime = self.get_playtime()
|
||||||
@ -1631,7 +1649,7 @@ class CartDialog(QDialog):
|
|||||||
"""Edit cart details"""
|
"""Edit cart details"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, musicmuster: Window, session: scoped_session, cart: Carts, *args, **kwargs
|
self, musicmuster: Window, session: Session, cart: Carts, *args, **kwargs
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Manage carts
|
Manage carts
|
||||||
@ -1756,18 +1774,15 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Run as required
|
# Run as required
|
||||||
if args.check_db:
|
if args.check_db:
|
||||||
log.debug("Updating database")
|
log.debug("Checking database")
|
||||||
with Session() as session:
|
with db.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 db.Session() as session:
|
||||||
update_bitrates(session)
|
update_bitrates(session)
|
||||||
engine.dispose()
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
# PyQt6 defaults to a grey for labels
|
# PyQt6 defaults to a grey for labels
|
||||||
palette = app.palette()
|
palette = app.palette()
|
||||||
@ -1785,7 +1800,6 @@ if __name__ == "__main__":
|
|||||||
win = Window()
|
win = Window()
|
||||||
win.show()
|
win.show()
|
||||||
status = app.exec()
|
status = app.exec()
|
||||||
engine.dispose()
|
|
||||||
sys.exit(status)
|
sys.exit(status)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if os.environ["MM_ENV"] == "PRODUCTION":
|
if os.environ["MM_ENV"] == "PRODUCTION":
|
||||||
|
|||||||
@ -81,22 +81,22 @@ import argparse
|
|||||||
|
|
||||||
|
|
||||||
if sys.version_info[0] < 3:
|
if sys.version_info[0] < 3:
|
||||||
raise RuntimeError('PipeClient Error: Python 3.x required')
|
raise RuntimeError("PipeClient Error: Python 3.x required")
|
||||||
|
|
||||||
# Platform specific constants
|
# Platform specific constants
|
||||||
if sys.platform == 'win32':
|
if sys.platform == "win32":
|
||||||
WRITE_NAME: str = '\\\\.\\pipe\\ToSrvPipe'
|
WRITE_NAME: str = "\\\\.\\pipe\\ToSrvPipe"
|
||||||
READ_NAME: str = '\\\\.\\pipe\\FromSrvPipe'
|
READ_NAME: str = "\\\\.\\pipe\\FromSrvPipe"
|
||||||
EOL: str = '\r\n\0'
|
EOL: str = "\r\n\0"
|
||||||
else:
|
else:
|
||||||
# Linux or Mac
|
# Linux or Mac
|
||||||
PIPE_BASE: str = '/tmp/audacity_script_pipe.'
|
PIPE_BASE: str = "/tmp/audacity_script_pipe."
|
||||||
WRITE_NAME: str = PIPE_BASE + 'to.' + str(os.getuid())
|
WRITE_NAME: str = PIPE_BASE + "to." + str(os.getuid())
|
||||||
READ_NAME: str = PIPE_BASE + 'from.' + str(os.getuid())
|
READ_NAME: str = PIPE_BASE + "from." + str(os.getuid())
|
||||||
EOL: str = '\n'
|
EOL: str = "\n"
|
||||||
|
|
||||||
|
|
||||||
class PipeClient():
|
class PipeClient:
|
||||||
"""Write / read client access to Audacity via named pipes.
|
"""Write / read client access to Audacity via named pipes.
|
||||||
|
|
||||||
Normally there should be just one instance of this class. If
|
Normally there should be just one instance of this class. If
|
||||||
@ -141,7 +141,7 @@ class PipeClient():
|
|||||||
self.timer: bool = False # type: ignore
|
self.timer: bool = False # type: ignore
|
||||||
self._start_time: float = 0 # type: ignore
|
self._start_time: float = 0 # type: ignore
|
||||||
self._write_pipe = None
|
self._write_pipe = None
|
||||||
self.reply: str = '' # type: ignore
|
self.reply: str = "" # type: ignore
|
||||||
if not self._write_pipe:
|
if not self._write_pipe:
|
||||||
self._write_thread_start()
|
self._write_thread_start()
|
||||||
self._read_thread_start()
|
self._read_thread_start()
|
||||||
@ -156,11 +156,11 @@ class PipeClient():
|
|||||||
# Allow a little time for connection to be made.
|
# Allow a little time for connection to be made.
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if not self._write_pipe:
|
if not self._write_pipe:
|
||||||
raise RuntimeError('PipeClientError: Write pipe cannot be opened.')
|
raise RuntimeError("PipeClientError: Write pipe cannot be opened.")
|
||||||
|
|
||||||
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."""
|
||||||
@ -187,16 +187,16 @@ class PipeClient():
|
|||||||
self._write_pipe.write(command + EOL)
|
self._write_pipe.write(command + EOL)
|
||||||
# Check that read pipe is alive
|
# Check that read pipe is alive
|
||||||
if PipeClient.reader_pipe_broken.is_set():
|
if PipeClient.reader_pipe_broken.is_set():
|
||||||
raise RuntimeError('PipeClient: Read-pipe error.')
|
raise RuntimeError("PipeClient: Read-pipe error.")
|
||||||
try:
|
try:
|
||||||
self._write_pipe.flush()
|
self._write_pipe.flush()
|
||||||
if self.timer:
|
if self.timer:
|
||||||
self._start_time = time.time()
|
self._start_time = time.time()
|
||||||
self.reply = ''
|
self.reply = ""
|
||||||
PipeClient.reply_ready.clear()
|
PipeClient.reply_ready.clear()
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
if err.errno == errno.EPIPE:
|
if err.errno == errno.EPIPE:
|
||||||
raise RuntimeError('PipeClient: Write-pipe error.')
|
raise RuntimeError("PipeClient: Write-pipe error.")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@ -204,27 +204,27 @@ 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()
|
||||||
# Stop timer as soon as we get first line of response.
|
# Stop timer as soon as we get first line of response.
|
||||||
stop_time = time.time()
|
stop_time = time.time()
|
||||||
while pipe_ok and line != '\n':
|
while pipe_ok and line != "\n":
|
||||||
message += line
|
message += line
|
||||||
line = read_pipe.readline()
|
line = read_pipe.readline()
|
||||||
if line == '':
|
if line == "":
|
||||||
# No data in read_pipe indicates that the pipe
|
# No data in read_pipe indicates that the pipe
|
||||||
# is broken (Audacity may have crashed).
|
# is broken (Audacity may have crashed).
|
||||||
PipeClient.reader_pipe_broken.set()
|
PipeClient.reader_pipe_broken.set()
|
||||||
pipe_ok = False
|
pipe_ok = False
|
||||||
if self.timer:
|
if self.timer:
|
||||||
xtime = (stop_time - self._start_time) * 1000
|
xtime = (stop_time - self._start_time) * 1000
|
||||||
message += f'Execution time: {xtime:.2f}ms'
|
message += f"Execution time: {xtime:.2f}ms"
|
||||||
self.reply = message
|
self.reply = message
|
||||||
PipeClient.reply_ready.set()
|
PipeClient.reply_ready.set()
|
||||||
message = ''
|
message = ""
|
||||||
|
|
||||||
def read(self) -> str:
|
def read(self) -> str:
|
||||||
"""Read Audacity's reply from pipe.
|
"""Read Audacity's reply from pipe.
|
||||||
@ -238,31 +238,45 @@ class PipeClient():
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if not PipeClient.reply_ready.is_set():
|
if not PipeClient.reply_ready.is_set():
|
||||||
return ''
|
return ""
|
||||||
return self.reply
|
return self.reply
|
||||||
|
|
||||||
|
|
||||||
def bool_from_string(strval) -> bool:
|
def bool_from_string(strval) -> bool:
|
||||||
"""Return boolean value from string"""
|
"""Return boolean value from string"""
|
||||||
if strval.lower() in ('true', 't', '1', 'yes', 'y'):
|
if strval.lower() in ("true", "t", "1", "yes", "y"):
|
||||||
return True
|
return True
|
||||||
if strval.lower() in ('false', 'f', '0', 'no', 'n'):
|
if strval.lower() in ("false", "f", "0", "no", "n"):
|
||||||
return False
|
return False
|
||||||
raise argparse.ArgumentTypeError('Boolean value expected.')
|
raise argparse.ArgumentTypeError("Boolean value expected.")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Interactive command-line for PipeClient"""
|
"""Interactive command-line for PipeClient"""
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-t', '--timeout', type=float, metavar='', default=10,
|
parser.add_argument(
|
||||||
help="timeout for reply in seconds (default: 10")
|
"-t",
|
||||||
parser.add_argument('-s', '--show-time', metavar='True/False',
|
"--timeout",
|
||||||
nargs='?', type=bool_from_string,
|
type=float,
|
||||||
const='t', default='t', dest='show',
|
metavar="",
|
||||||
help='show command execution time (default: True)')
|
default=10,
|
||||||
parser.add_argument('-d', '--docs', action='store_true',
|
help="timeout for reply in seconds (default: 10",
|
||||||
help='show documentation and exit')
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s",
|
||||||
|
"--show-time",
|
||||||
|
metavar="True/False",
|
||||||
|
nargs="?",
|
||||||
|
type=bool_from_string,
|
||||||
|
const="t",
|
||||||
|
default="t",
|
||||||
|
dest="show",
|
||||||
|
help="show command execution time (default: True)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d", "--docs", action="store_true", help="show documentation and exit"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.docs:
|
if args.docs:
|
||||||
@ -271,23 +285,23 @@ def main() -> None:
|
|||||||
|
|
||||||
client: PipeClient = PipeClient()
|
client: PipeClient = PipeClient()
|
||||||
while True:
|
while True:
|
||||||
reply: str = ''
|
reply: str = ""
|
||||||
message: str = input("\nEnter command or 'Q' to quit: ")
|
message: str = input("\nEnter command or 'Q' to quit: ")
|
||||||
start = time.time()
|
start = time.time()
|
||||||
if message.upper() == 'Q':
|
if message.upper() == "Q":
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif message == '':
|
elif message == "":
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
client.write(message, timer=args.show)
|
client.write(message, timer=args.show)
|
||||||
while reply == '':
|
while reply == "":
|
||||||
time.sleep(0.1) # allow time for reply
|
time.sleep(0.1) # allow time for reply
|
||||||
if time.time() - start > args.timeout:
|
if time.time() - start > args.timeout:
|
||||||
reply = 'PipeClient: Reply timed-out.'
|
reply = "PipeClient: Reply timed-out."
|
||||||
else:
|
else:
|
||||||
reply = client.read()
|
reply = client.read()
|
||||||
print(reply)
|
print(reply)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
|
# Standard library imports
|
||||||
# Allow forward reference to PlaylistModel
|
# Allow forward reference to PlaylistModel
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import obsws_python as obs # type: ignore
|
|
||||||
import re
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from enum import auto, Enum
|
from enum import auto, Enum
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
import datetime as dt
|
||||||
|
import re
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QAbstractTableModel,
|
QAbstractTableModel,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
@ -23,9 +25,14 @@ from PyQt6.QtGui import (
|
|||||||
QFont,
|
QFont,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
import obsws_python as obs # type: ignore
|
||||||
|
|
||||||
|
# import snoop # type: ignore
|
||||||
|
|
||||||
|
# App imports
|
||||||
from classes import track_sequence, MusicMusterSignals, PlaylistTrack
|
from classes import track_sequence, MusicMusterSignals, PlaylistTrack
|
||||||
from config import Config
|
from config import Config
|
||||||
from dbconfig import scoped_session, Session
|
|
||||||
from helpers import (
|
from helpers import (
|
||||||
file_is_unreadable,
|
file_is_unreadable,
|
||||||
get_embedded_time,
|
get_embedded_time,
|
||||||
@ -34,7 +41,7 @@ from helpers import (
|
|||||||
set_track_metadata,
|
set_track_metadata,
|
||||||
)
|
)
|
||||||
from log import log
|
from log import log
|
||||||
from models import NoteColours, Playdates, PlaylistRows, Tracks
|
from models import db, NoteColours, Playdates, PlaylistRows, Tracks
|
||||||
|
|
||||||
|
|
||||||
HEADER_NOTES_COLUMN = 1
|
HEADER_NOTES_COLUMN = 1
|
||||||
@ -62,13 +69,13 @@ class PlaylistRowData:
|
|||||||
self.artist: str = ""
|
self.artist: str = ""
|
||||||
self.bitrate = 0
|
self.bitrate = 0
|
||||||
self.duration: int = 0
|
self.duration: int = 0
|
||||||
self.lastplayed: datetime = Config.EPOCH
|
self.lastplayed: dt.datetime = Config.EPOCH
|
||||||
self.path = ""
|
self.path = ""
|
||||||
self.played = False
|
self.played = False
|
||||||
self.start_gap: Optional[int] = None
|
self.start_gap: Optional[int] = None
|
||||||
self.title: str = ""
|
self.title: str = ""
|
||||||
self.start_time: Optional[datetime] = None
|
self.start_time: Optional[dt.datetime] = None
|
||||||
self.end_time: Optional[datetime] = None
|
self.end_time: Optional[dt.datetime] = None
|
||||||
|
|
||||||
self.plrid: int = plr.id
|
self.plrid: int = plr.id
|
||||||
self.plr_rownum: int = plr.plr_rownum
|
self.plr_rownum: int = plr.plr_rownum
|
||||||
@ -116,7 +123,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
log.info(f"PlaylistModel.__init__({playlist_id=})")
|
log.debug(f"PlaylistModel.__init__({playlist_id=})")
|
||||||
|
|
||||||
self.playlist_id = playlist_id
|
self.playlist_id = playlist_id
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -129,7 +136,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
||||||
self.signals.row_order_changed_signal.connect(self.row_order_changed)
|
self.signals.row_order_changed_signal.connect(self.row_order_changed)
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
# Ensure row numbers in playlist are contiguous
|
# Ensure row numbers in playlist are contiguous
|
||||||
PlaylistRows.fixup_rownumbers(session, playlist_id)
|
PlaylistRows.fixup_rownumbers(session, playlist_id)
|
||||||
# Populate self.playlist_rows
|
# Populate self.playlist_rows
|
||||||
@ -148,7 +155,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Add track to existing header row
|
Add track to existing header row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(f"add_track_to_header({row_number=}, {track_id=}, {note=}")
|
log.debug(f"add_track_to_header({row_number=}, {track_id=}, {note=}")
|
||||||
|
|
||||||
# Get existing row
|
# Get existing row
|
||||||
try:
|
try:
|
||||||
@ -165,7 +172,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
"Header row already has track associated"
|
"Header row already has track associated"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
plr = session.get(PlaylistRows, prd.plrid)
|
plr = session.get(PlaylistRows, prd.plrid)
|
||||||
if plr:
|
if plr:
|
||||||
# Add track to PlaylistRows
|
# Add track to PlaylistRows
|
||||||
@ -187,7 +194,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Header row
|
# Header row
|
||||||
if self.is_header_row(row):
|
if self.is_header_row(row):
|
||||||
# Check for specific header colouring
|
# Check for specific header colouring
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
note_colour = NoteColours.get_colour(session, prd.note)
|
note_colour = NoteColours.get_colour(session, prd.note)
|
||||||
if note_colour:
|
if note_colour:
|
||||||
return QBrush(QColor(note_colour))
|
return QBrush(QColor(note_colour))
|
||||||
@ -216,7 +223,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
|
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
|
||||||
if column == Col.NOTE.value:
|
if column == Col.NOTE.value:
|
||||||
if prd.note:
|
if prd.note:
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
note_colour = NoteColours.get_colour(session, prd.note)
|
note_colour = NoteColours.get_colour(session, prd.note)
|
||||||
if note_colour:
|
if note_colour:
|
||||||
return QBrush(QColor(note_colour))
|
return QBrush(QColor(note_colour))
|
||||||
@ -275,7 +282,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
log.debug("Call OBS scene change")
|
log.debug("Call OBS scene change")
|
||||||
self.obs_scene_change(row_number)
|
self.obs_scene_change(row_number)
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
# Update Playdates in database
|
# Update Playdates in database
|
||||||
log.debug("update playdates")
|
log.debug("update playdates")
|
||||||
Playdates(session, track_sequence.now.track_id)
|
Playdates(session, track_sequence.now.track_id)
|
||||||
@ -377,7 +384,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Delete from highest row back so that not yet deleted row numbers don't change.
|
Delete from highest row back so that not yet deleted row numbers don't change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
for row_number in sorted(row_numbers, reverse=True):
|
for row_number in sorted(row_numbers, reverse=True):
|
||||||
log.info(f"delete_rows(), {row_number=}")
|
log.info(f"delete_rows(), {row_number=}")
|
||||||
super().beginRemoveRows(QModelIndex(), row_number, row_number)
|
super().beginRemoveRows(QModelIndex(), row_number, row_number)
|
||||||
@ -454,7 +461,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if playlist_id != self.playlist_id:
|
if playlist_id != self.playlist_id:
|
||||||
log.debug(f"end_reset_model: not us ({self.playlist_id=})")
|
log.debug(f"end_reset_model: not us ({self.playlist_id=})")
|
||||||
return
|
return
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
super().endResetModel()
|
super().endResetModel()
|
||||||
self.reset_track_sequence_row_numbers()
|
self.reset_track_sequence_row_numbers()
|
||||||
@ -541,7 +548,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
If not given, return row number to add to end of model.
|
If not given, return row number to add to end of model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(f"_get_new_row_number({proposed_row_number=})")
|
log.debug(f"_get_new_row_number({proposed_row_number=})")
|
||||||
|
|
||||||
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
|
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
|
||||||
# We are adding to the end of the list
|
# We are adding to the end of the list
|
||||||
@ -552,7 +559,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
else:
|
else:
|
||||||
new_row_number = proposed_row_number
|
new_row_number = proposed_row_number
|
||||||
|
|
||||||
log.info(f"get_new_row_number() return: {new_row_number=}")
|
log.debug(f"get_new_row_number() return: {new_row_number=}")
|
||||||
return new_row_number
|
return new_row_number
|
||||||
|
|
||||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||||
@ -688,8 +695,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
< prd.plr_rownum
|
< prd.plr_rownum
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
section_end_time = track_sequence.now.end_time + timedelta(
|
section_end_time = (
|
||||||
milliseconds=duration
|
track_sequence.now.end_time
|
||||||
|
+ dt.timedelta(milliseconds=duration)
|
||||||
)
|
)
|
||||||
end_time_str = (
|
end_time_str = (
|
||||||
", section end time "
|
", section end time "
|
||||||
@ -744,23 +752,25 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self,
|
self,
|
||||||
proposed_row_number: Optional[int],
|
proposed_row_number: Optional[int],
|
||||||
track_id: Optional[int] = None,
|
track_id: Optional[int] = None,
|
||||||
note: Optional[str] = None,
|
note: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Insert a row.
|
Insert a row.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(f"insert_row({proposed_row_number=}, {track_id=}, {note=})")
|
log.debug(f"insert_row({proposed_row_number=}, {track_id=}, {note=})")
|
||||||
|
|
||||||
new_row_number = self._get_new_row_number(proposed_row_number)
|
new_row_number = self._get_new_row_number(proposed_row_number)
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
super().beginInsertRows(QModelIndex(), new_row_number, new_row_number)
|
super().beginInsertRows(QModelIndex(), new_row_number, new_row_number)
|
||||||
plr = PlaylistRows.insert_row(session, self.playlist_id, new_row_number)
|
_ = PlaylistRows.insert_row(
|
||||||
|
session=session,
|
||||||
plr.track_id = track_id
|
playlist_id=self.playlist_id,
|
||||||
if note:
|
new_row_number=new_row_number,
|
||||||
plr.note = note
|
note=note,
|
||||||
|
track_id=track_id,
|
||||||
|
)
|
||||||
|
|
||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
super().endInsertRows()
|
super().endInsertRows()
|
||||||
@ -820,7 +830,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Mark row as unplayed
|
Mark row as unplayed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
for row_number in row_numbers:
|
for row_number in row_numbers:
|
||||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||||
if not plr:
|
if not plr:
|
||||||
@ -835,7 +845,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Move the playlist rows given to to_row and below.
|
Move the playlist rows given to to_row and below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(f"move_rows({from_rows=}, {to_row_number=}")
|
log.debug(f"move_rows({from_rows=}, {to_row_number=}")
|
||||||
|
|
||||||
# Build a {current_row_number: new_row_number} dictionary
|
# Build a {current_row_number: new_row_number} dictionary
|
||||||
row_map: dict[int, int] = {}
|
row_map: dict[int, int] = {}
|
||||||
@ -883,7 +893,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
plrid = self.playlist_rows[oldrow].plrid
|
plrid = self.playlist_rows[oldrow].plrid
|
||||||
sqla_map.append({"plrid": plrid, "plr_rownum": newrow})
|
sqla_map.append({"plrid": plrid, "plr_rownum": newrow})
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
PlaylistRows.update_plr_rownumbers(session, self.playlist_id, sqla_map)
|
PlaylistRows.update_plr_rownumbers(session, self.playlist_id, sqla_map)
|
||||||
# Update playlist_rows
|
# Update playlist_rows
|
||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
@ -899,7 +909,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Move the playlist rows given to to_row and below of to_playlist.
|
Move the playlist rows given to to_row and below of to_playlist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(
|
log.debug(
|
||||||
f"move_rows_between_playlists({from_rows=}, {to_row_number=}, {to_playlist_id=}"
|
f"move_rows_between_playlists({from_rows=}, {to_row_number=}, {to_playlist_id=}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -912,7 +922,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Prepare destination playlist for a reset
|
# Prepare destination playlist for a reset
|
||||||
self.signals.begin_reset_model_signal.emit(to_playlist_id)
|
self.signals.begin_reset_model_signal.emit(to_playlist_id)
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
# Make room in destination playlist
|
# Make room in destination playlist
|
||||||
max_destination_row_number = PlaylistRows.get_last_used_row(
|
max_destination_row_number = PlaylistRows.get_last_used_row(
|
||||||
session, to_playlist_id
|
session, to_playlist_id
|
||||||
@ -962,7 +972,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
log.info(f"move_track_add_note({new_row_number=}, {existing_prd=}, {note=}")
|
log.info(f"move_track_add_note({new_row_number=}, {existing_prd=}, {note=}")
|
||||||
|
|
||||||
if note:
|
if note:
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
plr = session.get(PlaylistRows, existing_prd.plrid)
|
plr = session.get(PlaylistRows, existing_prd.plrid)
|
||||||
if plr:
|
if plr:
|
||||||
if plr.note:
|
if plr.note:
|
||||||
@ -1050,7 +1060,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Update display
|
# Update display
|
||||||
self.invalidate_row(track_sequence.previous.plr_rownum)
|
self.invalidate_row(track_sequence.previous.plr_rownum)
|
||||||
|
|
||||||
def refresh_data(self, session: scoped_session):
|
def refresh_data(self, session: db.session):
|
||||||
"""Populate dicts for data calls"""
|
"""Populate dicts for data calls"""
|
||||||
|
|
||||||
# Populate self.playlist_rows with playlist data
|
# Populate self.playlist_rows with playlist data
|
||||||
@ -1071,7 +1081,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
log.info(f"remove_track({row_number=})")
|
log.info(f"remove_track({row_number=})")
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||||
if plr:
|
if plr:
|
||||||
plr.track_id = None
|
plr.track_id = None
|
||||||
@ -1085,7 +1095,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
track_id = self.playlist_rows[row_number].track_id
|
track_id = self.playlist_rows[row_number].track_id
|
||||||
if track_id:
|
if track_id:
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
track = session.get(Tracks, track_id)
|
track = session.get(Tracks, track_id)
|
||||||
set_track_metadata(track)
|
set_track_metadata(track)
|
||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
@ -1097,11 +1107,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Signal handler for when row ordering has changed
|
Signal handler for when row ordering has changed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info("reset_track_sequence_row_numbers()")
|
log.debug("reset_track_sequence_row_numbers()")
|
||||||
|
|
||||||
# Check the track_sequence next, now and previous plrs and
|
# Check the track_sequence next, now and previous plrs and
|
||||||
# update the row number
|
# update the row number
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
if track_sequence.next.plr_rownum:
|
if track_sequence.next.plr_rownum:
|
||||||
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
|
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
|
||||||
if next_plr:
|
if next_plr:
|
||||||
@ -1129,7 +1139,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(f"_reversed_contiguous_row_groups({row_numbers=} called")
|
log.debug(f"_reversed_contiguous_row_groups({row_numbers=} called")
|
||||||
|
|
||||||
result: List[List[int]] = []
|
result: List[List[int]] = []
|
||||||
temp: List[int] = []
|
temp: List[int] = []
|
||||||
@ -1145,7 +1155,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
result.append(temp)
|
result.append(temp)
|
||||||
result.reverse()
|
result.reverse()
|
||||||
|
|
||||||
log.info(f"_reversed_contiguous_row_groups() returned: {result=}")
|
log.debug(f"_reversed_contiguous_row_groups() returned: {result=}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
||||||
@ -1158,7 +1168,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Signal handler for when row ordering has changed
|
Signal handler for when row ordering has changed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(f"row_order_changed({playlist_id=}) {self.playlist_id=}")
|
log.debug(f"row_order_changed({playlist_id=}) {self.playlist_id=}")
|
||||||
|
|
||||||
# Only action if this is for us
|
# Only action if this is for us
|
||||||
if playlist_id != self.playlist_id:
|
if playlist_id != self.playlist_id:
|
||||||
@ -1205,8 +1215,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals.next_track_changed_signal.emit()
|
self.signals.next_track_changed_signal.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update playing_track
|
# Update track_sequence
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
track_sequence.next = PlaylistTrack()
|
track_sequence.next = PlaylistTrack()
|
||||||
try:
|
try:
|
||||||
plrid = self.playlist_rows[row_number].plrid
|
plrid = self.playlist_rows[row_number].plrid
|
||||||
@ -1246,7 +1256,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
row_number = index.row()
|
row_number = index.row()
|
||||||
column = index.column()
|
column = index.column()
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||||
if not plr:
|
if not plr:
|
||||||
print(
|
print(
|
||||||
@ -1342,12 +1352,15 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Update track start/end times in self.playlist_rows
|
Update track start/end times in self.playlist_rows
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info("update_track_times()")
|
log.debug("update_track_times()")
|
||||||
|
|
||||||
next_start_time: Optional[datetime] = None
|
next_start_time: Optional[dt.datetime] = None
|
||||||
update_rows: List[int] = []
|
update_rows: List[int] = []
|
||||||
|
playlist_length = len(self.playlist_rows)
|
||||||
|
if not playlist_length:
|
||||||
|
return
|
||||||
|
|
||||||
for row_number in range(len(self.playlist_rows)):
|
for row_number in range(playlist_length):
|
||||||
prd = self.playlist_rows[row_number]
|
prd = self.playlist_rows[row_number]
|
||||||
|
|
||||||
# Reset start_time if this is the current row
|
# Reset start_time if this is the current row
|
||||||
@ -1365,7 +1378,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
and track_sequence.now.end_time
|
and track_sequence.now.end_time
|
||||||
):
|
):
|
||||||
prd.start_time = track_sequence.now.end_time
|
prd.start_time = track_sequence.now.end_time
|
||||||
prd.end_time = prd.start_time + timedelta(milliseconds=prd.duration)
|
prd.end_time = prd.start_time + dt.timedelta(milliseconds=prd.duration)
|
||||||
next_start_time = prd.end_time
|
next_start_time = prd.end_time
|
||||||
update_rows.append(row_number)
|
update_rows.append(row_number)
|
||||||
continue
|
continue
|
||||||
@ -1410,7 +1423,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
update_rows.append(row_number)
|
update_rows.append(row_number)
|
||||||
|
|
||||||
# Calculate next start time
|
# Calculate next start time
|
||||||
next_start_time += timedelta(milliseconds=prd.duration)
|
next_start_time += dt.timedelta(milliseconds=prd.duration)
|
||||||
|
|
||||||
# Update end time of this row if it's incorrect
|
# Update end time of this row if it's incorrect
|
||||||
if prd.end_time != next_start_time:
|
if prd.end_time != next_start_time:
|
||||||
@ -1455,7 +1468,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
if self.source_model.played_tracks_hidden:
|
if self.source_model.played_tracks_hidden:
|
||||||
if self.source_model.is_played_row(source_row):
|
if self.source_model.is_played_row(source_row):
|
||||||
# Don't hide current or next track
|
# Don't hide current or next track
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
if track_sequence.next.plr_id:
|
if track_sequence.next.plr_id:
|
||||||
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
|
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
|
||||||
if (
|
if (
|
||||||
@ -1486,9 +1499,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
== self.source_model.playlist_id
|
== self.source_model.playlist_id
|
||||||
):
|
):
|
||||||
if track_sequence.now.start_time:
|
if track_sequence.now.start_time:
|
||||||
if datetime.now() > (
|
if dt.datetime.now() > (
|
||||||
track_sequence.now.start_time
|
track_sequence.now.start_time
|
||||||
+ timedelta(
|
+ dt.timedelta(
|
||||||
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
|
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
@ -1556,7 +1569,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: Optional[str] = None,
|
note: str = "",
|
||||||
) -> 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)
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
|
# Standard library imports
|
||||||
|
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
||||||
import psutil
|
import psutil
|
||||||
import time
|
import time
|
||||||
from pprint import pprint
|
|
||||||
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QEvent,
|
QEvent,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
@ -30,10 +31,12 @@ from PyQt6.QtWidgets import (
|
|||||||
QStyleOption,
|
QStyleOption,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dbconfig import Session
|
# Third party imports
|
||||||
from dialogs import TrackSelectDialog
|
|
||||||
|
# App imports
|
||||||
from classes import MusicMusterSignals, track_sequence
|
from classes import MusicMusterSignals, track_sequence
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from dialogs import TrackSelectDialog
|
||||||
from helpers import (
|
from helpers import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
ms_to_mmss,
|
ms_to_mmss,
|
||||||
@ -41,11 +44,11 @@ from helpers import (
|
|||||||
show_warning,
|
show_warning,
|
||||||
)
|
)
|
||||||
from log import log
|
from log import log
|
||||||
from models import Settings
|
from models import db, Settings
|
||||||
|
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from musicmuster import Window
|
from musicmuster import Window
|
||||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
|
||||||
|
|
||||||
|
|
||||||
class EscapeDelegate(QStyledItemDelegate):
|
class EscapeDelegate(QStyledItemDelegate):
|
||||||
@ -335,7 +338,7 @@ class PlaylistTab(QTableView):
|
|||||||
if model_row_number is None:
|
if model_row_number is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
dlg = TrackSelectDialog(
|
dlg = TrackSelectDialog(
|
||||||
session=session,
|
session=session,
|
||||||
new_row_number=model_row_number,
|
new_row_number=model_row_number,
|
||||||
@ -536,7 +539,7 @@ class PlaylistTab(QTableView):
|
|||||||
# Resize rows if necessary
|
# Resize rows if necessary
|
||||||
self.resizeRowsToContents()
|
self.resizeRowsToContents()
|
||||||
|
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
attr_name = f"playlist_col_{column_number}_width"
|
attr_name = f"playlist_col_{column_number}_width"
|
||||||
record = Settings.get_int_settings(session, attr_name)
|
record = Settings.get_int_settings(session, attr_name)
|
||||||
record.f_int = self.columnWidth(column_number)
|
record.f_int = self.columnWidth(column_number)
|
||||||
@ -830,7 +833,7 @@ class PlaylistTab(QTableView):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Last column is set to stretch so ignore it here
|
# Last column is set to stretch so ignore it here
|
||||||
with Session() as session:
|
with db.Session() as session:
|
||||||
for column_number in range(header.count() - 1):
|
for column_number in range(header.count() - 1):
|
||||||
attr_name = f"playlist_col_{column_number}_width"
|
attr_name = f"playlist_col_{column_number}_width"
|
||||||
record = Settings.get_int_settings(session, attr_name)
|
record = Settings.get_int_settings(session, attr_name)
|
||||||
|
|||||||
@ -4,20 +4,25 @@
|
|||||||
# 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
|
||||||
@ -42,7 +47,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("MM_DB"):
|
if "musicmuster_prod" not in os.environ.get("ALCHEMICAL_DATABASE_URI"):
|
||||||
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)
|
||||||
@ -51,7 +56,7 @@ def main():
|
|||||||
assert source_dir != parent_dir
|
assert source_dir != parent_dir
|
||||||
|
|
||||||
# Scan parent directory
|
# Scan parent directory
|
||||||
with Session() as session:
|
with db.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]
|
||||||
@ -239,7 +244,7 @@ def process_track(src, dst, title, artist, bitrate):
|
|||||||
if not do_processing:
|
if not do_processing:
|
||||||
return
|
return
|
||||||
|
|
||||||
with Session() as session:
|
with db.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
|
||||||
|
|||||||
@ -15,7 +15,11 @@ class Ui_MainWindow(object):
|
|||||||
MainWindow.resize(1280, 857)
|
MainWindow.resize(1280, 857)
|
||||||
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
|
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
|
||||||
icon = QtGui.QIcon()
|
icon = QtGui.QIcon()
|
||||||
icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon.addPixmap(
|
||||||
|
QtGui.QPixmap(":/icons/musicmuster"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
MainWindow.setWindowIcon(icon)
|
MainWindow.setWindowIcon(icon)
|
||||||
MainWindow.setStyleSheet("")
|
MainWindow.setStyleSheet("")
|
||||||
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
|
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
|
||||||
@ -27,39 +31,62 @@ class Ui_MainWindow(object):
|
|||||||
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
|
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
|
||||||
self.verticalLayout_3.setObjectName("verticalLayout_3")
|
self.verticalLayout_3.setObjectName("verticalLayout_3")
|
||||||
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
|
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
|
sizePolicy = QtWidgets.QSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth())
|
sizePolicy.setHeightForWidth(
|
||||||
|
self.previous_track_2.sizePolicy().hasHeightForWidth()
|
||||||
|
)
|
||||||
self.previous_track_2.setSizePolicy(sizePolicy)
|
self.previous_track_2.setSizePolicy(sizePolicy)
|
||||||
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
|
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("Sans")
|
font.setFamily("Sans")
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
self.previous_track_2.setFont(font)
|
self.previous_track_2.setFont(font)
|
||||||
self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n"
|
self.previous_track_2.setStyleSheet(
|
||||||
"border: 1px solid rgb(85, 87, 83);")
|
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
|
||||||
self.previous_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
|
)
|
||||||
|
self.previous_track_2.setAlignment(
|
||||||
|
QtCore.Qt.AlignmentFlag.AlignRight
|
||||||
|
| QtCore.Qt.AlignmentFlag.AlignTrailing
|
||||||
|
| QtCore.Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
self.previous_track_2.setObjectName("previous_track_2")
|
self.previous_track_2.setObjectName("previous_track_2")
|
||||||
self.verticalLayout_3.addWidget(self.previous_track_2)
|
self.verticalLayout_3.addWidget(self.previous_track_2)
|
||||||
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
|
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
|
sizePolicy = QtWidgets.QSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth())
|
sizePolicy.setHeightForWidth(
|
||||||
|
self.current_track_2.sizePolicy().hasHeightForWidth()
|
||||||
|
)
|
||||||
self.current_track_2.setSizePolicy(sizePolicy)
|
self.current_track_2.setSizePolicy(sizePolicy)
|
||||||
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
|
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setFamily("Sans")
|
font.setFamily("Sans")
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
self.current_track_2.setFont(font)
|
self.current_track_2.setFont(font)
|
||||||
self.current_track_2.setStyleSheet("background-color: #d4edda;\n"
|
self.current_track_2.setStyleSheet(
|
||||||
"border: 1px solid rgb(85, 87, 83);")
|
"background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);"
|
||||||
self.current_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
|
)
|
||||||
|
self.current_track_2.setAlignment(
|
||||||
|
QtCore.Qt.AlignmentFlag.AlignRight
|
||||||
|
| QtCore.Qt.AlignmentFlag.AlignTrailing
|
||||||
|
| QtCore.Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
self.current_track_2.setObjectName("current_track_2")
|
self.current_track_2.setObjectName("current_track_2")
|
||||||
self.verticalLayout_3.addWidget(self.current_track_2)
|
self.verticalLayout_3.addWidget(self.current_track_2)
|
||||||
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
|
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
|
sizePolicy = QtWidgets.QSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
|
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
|
||||||
@ -69,19 +96,29 @@ class Ui_MainWindow(object):
|
|||||||
font.setFamily("Sans")
|
font.setFamily("Sans")
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
self.next_track_2.setFont(font)
|
self.next_track_2.setFont(font)
|
||||||
self.next_track_2.setStyleSheet("background-color: #fff3cd;\n"
|
self.next_track_2.setStyleSheet(
|
||||||
"border: 1px solid rgb(85, 87, 83);")
|
"background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);"
|
||||||
self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
|
)
|
||||||
|
self.next_track_2.setAlignment(
|
||||||
|
QtCore.Qt.AlignmentFlag.AlignRight
|
||||||
|
| QtCore.Qt.AlignmentFlag.AlignTrailing
|
||||||
|
| QtCore.Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
self.next_track_2.setObjectName("next_track_2")
|
self.next_track_2.setObjectName("next_track_2")
|
||||||
self.verticalLayout_3.addWidget(self.next_track_2)
|
self.verticalLayout_3.addWidget(self.next_track_2)
|
||||||
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
|
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
|
||||||
self.verticalLayout = QtWidgets.QVBoxLayout()
|
self.verticalLayout = QtWidgets.QVBoxLayout()
|
||||||
self.verticalLayout.setObjectName("verticalLayout")
|
self.verticalLayout.setObjectName("verticalLayout")
|
||||||
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
|
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
|
sizePolicy = QtWidgets.QSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth())
|
sizePolicy.setHeightForWidth(
|
||||||
|
self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()
|
||||||
|
)
|
||||||
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
|
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
|
||||||
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
|
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
|
||||||
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
||||||
@ -89,32 +126,43 @@ class Ui_MainWindow(object):
|
|||||||
font.setFamily("Sans")
|
font.setFamily("Sans")
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
self.hdrPreviousTrack.setFont(font)
|
self.hdrPreviousTrack.setFont(font)
|
||||||
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
|
self.hdrPreviousTrack.setStyleSheet(
|
||||||
"border: 1px solid rgb(85, 87, 83);")
|
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
|
||||||
|
)
|
||||||
self.hdrPreviousTrack.setText("")
|
self.hdrPreviousTrack.setText("")
|
||||||
self.hdrPreviousTrack.setWordWrap(False)
|
self.hdrPreviousTrack.setWordWrap(False)
|
||||||
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
|
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
|
||||||
self.verticalLayout.addWidget(self.hdrPreviousTrack)
|
self.verticalLayout.addWidget(self.hdrPreviousTrack)
|
||||||
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
|
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
|
sizePolicy = QtWidgets.QSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth())
|
sizePolicy.setHeightForWidth(
|
||||||
|
self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()
|
||||||
|
)
|
||||||
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
|
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
|
||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
self.hdrCurrentTrack.setFont(font)
|
self.hdrCurrentTrack.setFont(font)
|
||||||
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
|
self.hdrCurrentTrack.setStyleSheet(
|
||||||
|
"background-color: #d4edda;\n"
|
||||||
"border: 1px solid rgb(85, 87, 83);\n"
|
"border: 1px solid rgb(85, 87, 83);\n"
|
||||||
"text-align: left;\n"
|
"text-align: left;\n"
|
||||||
"padding-left: 8px;\n"
|
"padding-left: 8px;\n"
|
||||||
"")
|
""
|
||||||
|
)
|
||||||
self.hdrCurrentTrack.setText("")
|
self.hdrCurrentTrack.setText("")
|
||||||
self.hdrCurrentTrack.setFlat(True)
|
self.hdrCurrentTrack.setFlat(True)
|
||||||
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
|
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
|
||||||
self.verticalLayout.addWidget(self.hdrCurrentTrack)
|
self.verticalLayout.addWidget(self.hdrCurrentTrack)
|
||||||
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
|
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
|
sizePolicy = QtWidgets.QSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
|
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
|
||||||
@ -122,10 +170,12 @@ class Ui_MainWindow(object):
|
|||||||
font = QtGui.QFont()
|
font = QtGui.QFont()
|
||||||
font.setPointSize(20)
|
font.setPointSize(20)
|
||||||
self.hdrNextTrack.setFont(font)
|
self.hdrNextTrack.setFont(font)
|
||||||
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
|
self.hdrNextTrack.setStyleSheet(
|
||||||
|
"background-color: #fff3cd;\n"
|
||||||
"border: 1px solid rgb(85, 87, 83);\n"
|
"border: 1px solid rgb(85, 87, 83);\n"
|
||||||
"text-align: left;\n"
|
"text-align: left;\n"
|
||||||
"padding-left: 8px;")
|
"padding-left: 8px;"
|
||||||
|
)
|
||||||
self.hdrNextTrack.setText("")
|
self.hdrNextTrack.setText("")
|
||||||
self.hdrNextTrack.setFlat(True)
|
self.hdrNextTrack.setFlat(True)
|
||||||
self.hdrNextTrack.setObjectName("hdrNextTrack")
|
self.hdrNextTrack.setObjectName("hdrNextTrack")
|
||||||
@ -160,7 +210,12 @@ class Ui_MainWindow(object):
|
|||||||
self.cartsWidget.setObjectName("cartsWidget")
|
self.cartsWidget.setObjectName("cartsWidget")
|
||||||
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
|
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
|
||||||
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
|
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
|
||||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
|
spacerItem = QtWidgets.QSpacerItem(
|
||||||
|
40,
|
||||||
|
20,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Expanding,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Minimum,
|
||||||
|
)
|
||||||
self.horizontalLayout_Carts.addItem(spacerItem)
|
self.horizontalLayout_Carts.addItem(spacerItem)
|
||||||
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
|
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
|
||||||
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
|
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
|
||||||
@ -205,7 +260,11 @@ class Ui_MainWindow(object):
|
|||||||
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
|
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
|
||||||
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
|
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
|
||||||
icon1 = QtGui.QIcon()
|
icon1 = QtGui.QIcon()
|
||||||
icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon1.addPixmap(
|
||||||
|
QtGui.QPixmap(":/icons/headphones"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.btnPreview.setIcon(icon1)
|
self.btnPreview.setIcon(icon1)
|
||||||
self.btnPreview.setIconSize(QtCore.QSize(30, 30))
|
self.btnPreview.setIconSize(QtCore.QSize(30, 30))
|
||||||
self.btnPreview.setCheckable(True)
|
self.btnPreview.setCheckable(True)
|
||||||
@ -289,10 +348,15 @@ class Ui_MainWindow(object):
|
|||||||
self.label_silent_timer.setObjectName("label_silent_timer")
|
self.label_silent_timer.setObjectName("label_silent_timer")
|
||||||
self.horizontalLayout.addWidget(self.frame_silent)
|
self.horizontalLayout.addWidget(self.frame_silent)
|
||||||
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
|
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
|
||||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
|
sizePolicy = QtWidgets.QSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
sizePolicy.setHorizontalStretch(1)
|
sizePolicy.setHorizontalStretch(1)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
|
sizePolicy.setHeightForWidth(
|
||||||
|
self.widgetFadeVolume.sizePolicy().hasHeightForWidth()
|
||||||
|
)
|
||||||
self.widgetFadeVolume.setSizePolicy(sizePolicy)
|
self.widgetFadeVolume.setSizePolicy(sizePolicy)
|
||||||
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
|
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
|
||||||
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
|
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
|
||||||
@ -309,7 +373,11 @@ class Ui_MainWindow(object):
|
|||||||
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
|
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
|
||||||
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
|
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
|
||||||
icon2 = QtGui.QIcon()
|
icon2 = QtGui.QIcon()
|
||||||
icon2.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon2.addPixmap(
|
||||||
|
QtGui.QPixmap(":/icons/fade"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.btnFade.setIcon(icon2)
|
self.btnFade.setIcon(icon2)
|
||||||
self.btnFade.setIconSize(QtCore.QSize(30, 30))
|
self.btnFade.setIconSize(QtCore.QSize(30, 30))
|
||||||
self.btnFade.setObjectName("btnFade")
|
self.btnFade.setObjectName("btnFade")
|
||||||
@ -317,7 +385,11 @@ class Ui_MainWindow(object):
|
|||||||
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
|
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
|
||||||
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
|
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
|
||||||
icon3 = QtGui.QIcon()
|
icon3 = QtGui.QIcon()
|
||||||
icon3.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon3.addPixmap(
|
||||||
|
QtGui.QPixmap(":/icons/stopsign"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.btnStop.setIcon(icon3)
|
self.btnStop.setIcon(icon3)
|
||||||
self.btnStop.setObjectName("btnStop")
|
self.btnStop.setObjectName("btnStop")
|
||||||
self.verticalLayout_5.addWidget(self.btnStop)
|
self.verticalLayout_5.addWidget(self.btnStop)
|
||||||
@ -343,39 +415,69 @@ class Ui_MainWindow(object):
|
|||||||
MainWindow.setStatusBar(self.statusbar)
|
MainWindow.setStatusBar(self.statusbar)
|
||||||
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
|
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
|
||||||
icon4 = QtGui.QIcon()
|
icon4 = QtGui.QIcon()
|
||||||
icon4.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon4.addPixmap(
|
||||||
|
QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.actionPlay_next.setIcon(icon4)
|
self.actionPlay_next.setIcon(icon4)
|
||||||
self.actionPlay_next.setObjectName("actionPlay_next")
|
self.actionPlay_next.setObjectName("actionPlay_next")
|
||||||
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
|
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
|
||||||
icon5 = QtGui.QIcon()
|
icon5 = QtGui.QIcon()
|
||||||
icon5.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon5.addPixmap(
|
||||||
|
QtGui.QPixmap(":/icons/next"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.actionSkipToNext.setIcon(icon5)
|
self.actionSkipToNext.setIcon(icon5)
|
||||||
self.actionSkipToNext.setObjectName("actionSkipToNext")
|
self.actionSkipToNext.setObjectName("actionSkipToNext")
|
||||||
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
|
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
|
||||||
icon6 = QtGui.QIcon()
|
icon6 = QtGui.QIcon()
|
||||||
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon6.addPixmap(
|
||||||
|
QtGui.QPixmap(
|
||||||
|
"app/ui/../../../../.designer/backup/icon_search_database.png"
|
||||||
|
),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.actionInsertTrack.setIcon(icon6)
|
self.actionInsertTrack.setIcon(icon6)
|
||||||
self.actionInsertTrack.setObjectName("actionInsertTrack")
|
self.actionInsertTrack.setObjectName("actionInsertTrack")
|
||||||
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
|
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
|
||||||
icon7 = QtGui.QIcon()
|
icon7 = QtGui.QIcon()
|
||||||
icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon7.addPixmap(
|
||||||
|
QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.actionAdd_file.setIcon(icon7)
|
self.actionAdd_file.setIcon(icon7)
|
||||||
self.actionAdd_file.setObjectName("actionAdd_file")
|
self.actionAdd_file.setObjectName("actionAdd_file")
|
||||||
self.actionFade = QtGui.QAction(parent=MainWindow)
|
self.actionFade = QtGui.QAction(parent=MainWindow)
|
||||||
icon8 = QtGui.QIcon()
|
icon8 = QtGui.QIcon()
|
||||||
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon8.addPixmap(
|
||||||
|
QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.actionFade.setIcon(icon8)
|
self.actionFade.setIcon(icon8)
|
||||||
self.actionFade.setObjectName("actionFade")
|
self.actionFade.setObjectName("actionFade")
|
||||||
self.actionStop = QtGui.QAction(parent=MainWindow)
|
self.actionStop = QtGui.QAction(parent=MainWindow)
|
||||||
icon9 = QtGui.QIcon()
|
icon9 = QtGui.QIcon()
|
||||||
icon9.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon9.addPixmap(
|
||||||
|
QtGui.QPixmap(":/icons/stop"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.actionStop.setIcon(icon9)
|
self.actionStop.setIcon(icon9)
|
||||||
self.actionStop.setObjectName("actionStop")
|
self.actionStop.setObjectName("actionStop")
|
||||||
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
|
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
|
||||||
self.action_Clear_selection.setObjectName("action_Clear_selection")
|
self.action_Clear_selection.setObjectName("action_Clear_selection")
|
||||||
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
|
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
|
||||||
icon10 = QtGui.QIcon()
|
icon10 = QtGui.QIcon()
|
||||||
icon10.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
|
icon10.addPixmap(
|
||||||
|
QtGui.QPixmap(":/icons/previous"),
|
||||||
|
QtGui.QIcon.Mode.Normal,
|
||||||
|
QtGui.QIcon.State.Off,
|
||||||
|
)
|
||||||
self.action_Resume_previous.setIcon(icon10)
|
self.action_Resume_previous.setIcon(icon10)
|
||||||
self.action_Resume_previous.setObjectName("action_Resume_previous")
|
self.action_Resume_previous.setObjectName("action_Resume_previous")
|
||||||
self.actionE_xit = QtGui.QAction(parent=MainWindow)
|
self.actionE_xit = QtGui.QAction(parent=MainWindow)
|
||||||
@ -422,7 +524,9 @@ class Ui_MainWindow(object):
|
|||||||
self.actionImport = QtGui.QAction(parent=MainWindow)
|
self.actionImport = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionImport.setObjectName("actionImport")
|
self.actionImport.setObjectName("actionImport")
|
||||||
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
|
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks")
|
self.actionDownload_CSV_of_played_tracks.setObjectName(
|
||||||
|
"actionDownload_CSV_of_played_tracks"
|
||||||
|
)
|
||||||
self.actionSearch = QtGui.QAction(parent=MainWindow)
|
self.actionSearch = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionSearch.setObjectName("actionSearch")
|
self.actionSearch.setObjectName("actionSearch")
|
||||||
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
|
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
|
||||||
@ -450,9 +554,13 @@ class Ui_MainWindow(object):
|
|||||||
self.actionResume = QtGui.QAction(parent=MainWindow)
|
self.actionResume = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionResume.setObjectName("actionResume")
|
self.actionResume.setObjectName("actionResume")
|
||||||
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
|
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia")
|
self.actionSearch_title_in_Wikipedia.setObjectName(
|
||||||
|
"actionSearch_title_in_Wikipedia"
|
||||||
|
)
|
||||||
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
|
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts")
|
self.actionSearch_title_in_Songfacts.setObjectName(
|
||||||
|
"actionSearch_title_in_Songfacts"
|
||||||
|
)
|
||||||
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
|
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
|
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
|
||||||
self.menuFile.addAction(self.actionNewPlaylist)
|
self.menuFile.addAction(self.actionNewPlaylist)
|
||||||
@ -539,38 +647,58 @@ class Ui_MainWindow(object):
|
|||||||
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
|
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
|
||||||
self.actionStop.setText(_translate("MainWindow", "S&top"))
|
self.actionStop.setText(_translate("MainWindow", "S&top"))
|
||||||
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
|
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
|
||||||
self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection"))
|
self.action_Clear_selection.setText(
|
||||||
|
_translate("MainWindow", "Clear &selection")
|
||||||
|
)
|
||||||
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
|
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
|
||||||
self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous"))
|
self.action_Resume_previous.setText(
|
||||||
|
_translate("MainWindow", "&Resume previous")
|
||||||
|
)
|
||||||
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
|
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
|
||||||
self.actionTest.setText(_translate("MainWindow", "&Test"))
|
self.actionTest.setText(_translate("MainWindow", "&Test"))
|
||||||
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
|
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
|
||||||
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
|
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
|
||||||
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
|
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
|
||||||
self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade"))
|
self.actionSkipToFade.setText(
|
||||||
|
_translate("MainWindow", "&Skip to start of fade")
|
||||||
|
)
|
||||||
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
|
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
|
||||||
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
|
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
|
||||||
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
|
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
|
||||||
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
|
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
|
||||||
self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to..."))
|
self.actionMoveSelected.setText(
|
||||||
|
_translate("MainWindow", "Mo&ve selected tracks to...")
|
||||||
|
)
|
||||||
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
|
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
|
||||||
self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
|
self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
|
||||||
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
|
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
|
||||||
self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track"))
|
self.actionSelect_next_track.setText(
|
||||||
|
_translate("MainWindow", "Select next track")
|
||||||
|
)
|
||||||
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
|
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
|
||||||
self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track"))
|
self.actionSelect_previous_track.setText(
|
||||||
|
_translate("MainWindow", "Select previous track")
|
||||||
|
)
|
||||||
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
|
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
|
||||||
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
|
self.actionSelect_played_tracks.setText(
|
||||||
self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
|
_translate("MainWindow", "Select played tracks")
|
||||||
|
)
|
||||||
|
self.actionMoveUnplayed.setText(
|
||||||
|
_translate("MainWindow", "Move &unplayed tracks to...")
|
||||||
|
)
|
||||||
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
|
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
|
||||||
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
|
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
|
||||||
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
|
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
|
||||||
self.actionImport.setText(_translate("MainWindow", "Import track..."))
|
self.actionImport.setText(_translate("MainWindow", "Import track..."))
|
||||||
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
|
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
|
||||||
self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks..."))
|
self.actionDownload_CSV_of_played_tracks.setText(
|
||||||
|
_translate("MainWindow", "Download CSV of played tracks...")
|
||||||
|
)
|
||||||
self.actionSearch.setText(_translate("MainWindow", "Search..."))
|
self.actionSearch.setText(_translate("MainWindow", "Search..."))
|
||||||
self.actionSearch.setShortcut(_translate("MainWindow", "/"))
|
self.actionSearch.setShortcut(_translate("MainWindow", "/"))
|
||||||
self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert §ion header..."))
|
self.actionInsertSectionHeader.setText(
|
||||||
|
_translate("MainWindow", "Insert §ion header...")
|
||||||
|
)
|
||||||
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
|
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
|
||||||
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
|
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
|
||||||
self.actionFind_next.setText(_translate("MainWindow", "Find next"))
|
self.actionFind_next.setText(_translate("MainWindow", "Find next"))
|
||||||
@ -578,8 +706,12 @@ class Ui_MainWindow(object):
|
|||||||
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
|
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
|
||||||
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
|
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
|
||||||
self.action_About.setText(_translate("MainWindow", "&About"))
|
self.action_About.setText(_translate("MainWindow", "&About"))
|
||||||
self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
|
self.actionSave_as_template.setText(
|
||||||
self.actionNew_from_template.setText(_translate("MainWindow", "New from template..."))
|
_translate("MainWindow", "Save as template...")
|
||||||
|
)
|
||||||
|
self.actionNew_from_template.setText(
|
||||||
|
_translate("MainWindow", "New from template...")
|
||||||
|
)
|
||||||
self.actionDebug.setText(_translate("MainWindow", "Debug"))
|
self.actionDebug.setText(_translate("MainWindow", "Debug"))
|
||||||
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
|
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
|
||||||
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
|
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
|
||||||
@ -588,10 +720,22 @@ class Ui_MainWindow(object):
|
|||||||
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
|
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
|
||||||
self.actionResume.setText(_translate("MainWindow", "Resume"))
|
self.actionResume.setText(_translate("MainWindow", "Resume"))
|
||||||
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
|
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
|
||||||
self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia"))
|
self.actionSearch_title_in_Wikipedia.setText(
|
||||||
self.actionSearch_title_in_Wikipedia.setShortcut(_translate("MainWindow", "Ctrl+W"))
|
_translate("MainWindow", "Search title in Wikipedia")
|
||||||
self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts"))
|
)
|
||||||
self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S"))
|
self.actionSearch_title_in_Wikipedia.setShortcut(
|
||||||
self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows..."))
|
_translate("MainWindow", "Ctrl+W")
|
||||||
|
)
|
||||||
|
self.actionSearch_title_in_Songfacts.setText(
|
||||||
|
_translate("MainWindow", "Search title in Songfacts")
|
||||||
|
)
|
||||||
|
self.actionSearch_title_in_Songfacts.setShortcut(
|
||||||
|
_translate("MainWindow", "Ctrl+S")
|
||||||
|
)
|
||||||
|
self.actionSelect_duplicate_rows.setText(
|
||||||
|
_translate("MainWindow", "Select duplicate rows...")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
from infotabs import InfoTabs
|
from infotabs import InfoTabs
|
||||||
from pyqtgraph import PlotWidget
|
from pyqtgraph import PlotWidget
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
import datetime as dt
|
||||||
from threading import Timer
|
from threading import Timer
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from timeloop import Timeloop
|
from timeloop import Timeloop # type: ignore
|
||||||
import vlc
|
import vlc # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class RepeatedTimer(object):
|
class RepeatedTimer(object):
|
||||||
@ -49,9 +49,9 @@ def leading_silence(audio_segment, silence_threshold=-50.0, chunk_size=10):
|
|||||||
|
|
||||||
trim_ms = 0 # ms
|
trim_ms = 0 # ms
|
||||||
assert chunk_size > 0 # to avoid infinite loop
|
assert chunk_size > 0 # to avoid infinite loop
|
||||||
while (
|
while audio_segment[
|
||||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < silence_threshold
|
trim_ms : trim_ms + chunk_size
|
||||||
and trim_ms < len(audio_segment)):
|
].dBFS < silence_threshold and trim_ms < len(audio_segment):
|
||||||
trim_ms += chunk_size
|
trim_ms += chunk_size
|
||||||
|
|
||||||
# if there is no end it should return the length of the segment
|
# if there is no end it should return the length of the segment
|
||||||
@ -73,7 +73,8 @@ def significant_fade(audio_segment, fade_threshold=-20.0, chunk_size=10):
|
|||||||
trim_ms = segment_length - chunk_size
|
trim_ms = segment_length - chunk_size
|
||||||
while (
|
while (
|
||||||
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
|
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
|
||||||
and trim_ms > 0):
|
and trim_ms > 0
|
||||||
|
):
|
||||||
trim_ms -= chunk_size
|
trim_ms -= chunk_size
|
||||||
|
|
||||||
# if there is no trailing silence, return lenght of track (it's less
|
# if there is no trailing silence, return lenght of track (it's less
|
||||||
@ -95,7 +96,8 @@ def trailing_silence(audio_segment, silence_threshold=-50.0, chunk_size=10):
|
|||||||
trim_ms = segment_length - chunk_size
|
trim_ms = segment_length - chunk_size
|
||||||
while (
|
while (
|
||||||
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < silence_threshold
|
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < silence_threshold
|
||||||
and trim_ms > 0):
|
and trim_ms > 0
|
||||||
|
):
|
||||||
trim_ms -= chunk_size
|
trim_ms -= chunk_size
|
||||||
|
|
||||||
# if there is no trailing silence, return lenght of track (it's less
|
# if there is no trailing silence, return lenght of track (it's less
|
||||||
@ -124,15 +126,17 @@ def update_progress(player, talk_at, silent_at):
|
|||||||
remaining_time = total_time - elapsed_time
|
remaining_time = total_time - elapsed_time
|
||||||
talk_time = remaining_time - (total_time - talk_at)
|
talk_time = remaining_time - (total_time - talk_at)
|
||||||
silent_time = remaining_time - (total_time - silent_at)
|
silent_time = remaining_time - (total_time - silent_at)
|
||||||
end_time = (datetime.now() + timedelta(
|
end_time = (dt.datetime.now() + timedelta(milliseconds=remaining_time)).strftime(
|
||||||
milliseconds=remaining_time)).strftime("%H:%M:%S")
|
"%H:%M:%S"
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f"\t{ms_to_mmss(elapsed_time)}/"
|
f"\t{ms_to_mmss(elapsed_time)}/"
|
||||||
f"{ms_to_mmss(total_time)}\t\t"
|
f"{ms_to_mmss(total_time)}\t\t"
|
||||||
f"Talk in: {ms_to_mmss(talk_time)} "
|
f"Talk in: {ms_to_mmss(talk_time)} "
|
||||||
f"Silent in: {ms_to_mmss(silent_time)} "
|
f"Silent in: {ms_to_mmss(silent_time)} "
|
||||||
f"Ends at: {end_time} [{ms_to_mmss(remaining_time)}]"
|
f"Ends at: {end_time} [{ms_to_mmss(remaining_time)}]",
|
||||||
, end="\r")
|
end="\r",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Print name of current song, print name of next song. Play current when
|
# Print name of current song, print name of next song. Play current when
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
# tl = Timeloop()
|
# tl = Timeloop()
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from PyQt5.QtCore import Qt
|
|||||||
|
|
||||||
qt_creator_file = "mainwindow.ui"
|
qt_creator_file = "mainwindow.ui"
|
||||||
Ui_MainWindow, QtBaseClass = uic.loadUiType(qt_creator_file)
|
Ui_MainWindow, QtBaseClass = uic.loadUiType(qt_creator_file)
|
||||||
tick = QtGui.QImage('tick.png')
|
tick = QtGui.QImage("tick.png")
|
||||||
|
|
||||||
|
|
||||||
class TodoModel(QtCore.QAbstractListModel):
|
class TodoModel(QtCore.QAbstractListModel):
|
||||||
@ -56,7 +56,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.model.todos.append((False, text))
|
self.model.todos.append((False, text))
|
||||||
# Trigger refresh.
|
# Trigger refresh.
|
||||||
self.model.layoutChanged.emit()
|
self.model.layoutChanged.emit()
|
||||||
# Empty the input
|
# Empty the input
|
||||||
self.todoEdit.setText("")
|
self.todoEdit.setText("")
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -88,13 +88,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
try:
|
try:
|
||||||
with open('data.db', 'r') as f:
|
with open("data.db", "r") as f:
|
||||||
self.model.todos = json.load(f)
|
self.model.todos = json.load(f)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
with open('data.db', 'w') as f:
|
with open("data.db", "w") as f:
|
||||||
data = json.dump(self.model.todos, f)
|
data = json.dump(self.model.todos, f)
|
||||||
|
|
||||||
|
|
||||||
@ -102,5 +102,3 @@ app = QtWidgets.QApplication(sys.argv)
|
|||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
49
conftest.py
49
conftest.py
@ -1,49 +0,0 @@
|
|||||||
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import helpers
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
||||||
|
|
||||||
from app.models import Base, Tracks
|
|
||||||
|
|
||||||
DB_CONNECTION = "mysql+mysqldb://musicmuster_testing:musicmuster_testing@localhost/dev_musicmuster_testing"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def db_engine():
|
|
||||||
engine = create_engine(DB_CONNECTION, isolation_level="READ COMMITTED")
|
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
yield engine
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def session(db_engine):
|
|
||||||
connection = db_engine.connect()
|
|
||||||
transaction = connection.begin()
|
|
||||||
sm = sessionmaker(bind=connection)
|
|
||||||
session = scoped_session(sm)
|
|
||||||
# print(f"PyTest SqlA: session acquired [{hex(id(session))}]")
|
|
||||||
yield session
|
|
||||||
# print(f" PyTest SqlA: session released and cleaned up [{hex(id(session))}]")
|
|
||||||
session.remove()
|
|
||||||
transaction.rollback()
|
|
||||||
connection.close()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def track1(session):
|
|
||||||
track_path = "testdata/isa.mp3"
|
|
||||||
metadata = helpers.get_file_metadata(track_path)
|
|
||||||
track = Tracks(session, **metadata)
|
|
||||||
return track
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def track2(session):
|
|
||||||
track_path = "testdata/mom.mp3"
|
|
||||||
metadata = helpers.get_file_metadata(track_path)
|
|
||||||
track = Tracks(session, **metadata)
|
|
||||||
return track
|
|
||||||
@ -10,23 +10,23 @@ import sqlalchemy as sa
|
|||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '0c604bf490f8'
|
revision = "0c604bf490f8"
|
||||||
down_revision = '29c0d7ffc741'
|
down_revision = "29c0d7ffc741"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.add_column('playlist_rows', sa.Column('played', sa.Boolean(), nullable=False))
|
op.add_column("playlist_rows", sa.Column("played", sa.Boolean(), nullable=False))
|
||||||
op.drop_index('ix_tracks_lastplayed', table_name='tracks')
|
op.drop_index("ix_tracks_lastplayed", table_name="tracks")
|
||||||
op.drop_column('tracks', 'lastplayed')
|
op.drop_column("tracks", "lastplayed")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.add_column('tracks', sa.Column('lastplayed', mysql.DATETIME(), nullable=True))
|
op.add_column("tracks", sa.Column("lastplayed", mysql.DATETIME(), nullable=True))
|
||||||
op.create_index('ix_tracks_lastplayed', 'tracks', ['lastplayed'], unique=False)
|
op.create_index("ix_tracks_lastplayed", "tracks", ["lastplayed"], unique=False)
|
||||||
op.drop_column('playlist_rows', 'played')
|
op.drop_column("playlist_rows", "played")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@ -10,21 +10,21 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '2cc37d3cf07f'
|
revision = "2cc37d3cf07f"
|
||||||
down_revision = 'e3b04db5506f'
|
down_revision = "e3b04db5506f"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.add_column('playlists', sa.Column('last_used', sa.DateTime(), nullable=True))
|
op.add_column("playlists", sa.Column("last_used", sa.DateTime(), nullable=True))
|
||||||
op.add_column('playlists', sa.Column('loaded', sa.Boolean(), nullable=True))
|
op.add_column("playlists", sa.Column("loaded", sa.Boolean(), nullable=True))
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_column('playlists', 'loaded')
|
op.drop_column("playlists", "loaded")
|
||||||
op.drop_column('playlists', 'last_used')
|
op.drop_column("playlists", "last_used")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@ -10,27 +10,28 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'b0983648595e'
|
revision = "b0983648595e"
|
||||||
down_revision = '1bc727e5e87f'
|
down_revision = "1bc727e5e87f"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('settings',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"settings",
|
||||||
sa.Column('name', sa.String(length=32), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('f_datetime', sa.DateTime(), nullable=True),
|
sa.Column("name", sa.String(length=32), nullable=False),
|
||||||
sa.Column('f_int', sa.Integer(), nullable=True),
|
sa.Column("f_datetime", sa.DateTime(), nullable=True),
|
||||||
sa.Column('f_string', sa.String(length=128), nullable=True),
|
sa.Column("f_int", sa.Integer(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.Column("f_string", sa.String(length=128), nullable=True),
|
||||||
sa.UniqueConstraint('name')
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('settings')
|
op.drop_table("settings")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@ -10,43 +10,54 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'f07b96a5e60f'
|
revision = "f07b96a5e60f"
|
||||||
down_revision = 'b0983648595e'
|
down_revision = "b0983648595e"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('playdates',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"playdates",
|
||||||
sa.Column('lastplayed', sa.DateTime(), nullable=True),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("lastplayed", sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_playdates_lastplayed'), 'playdates', ['lastplayed'], unique=False)
|
op.create_index(
|
||||||
op.create_table('playlists',
|
op.f("ix_playdates_lastplayed"), "playdates", ["lastplayed"], unique=False
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=32), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('name')
|
|
||||||
)
|
)
|
||||||
op.create_table('playlistracks',
|
op.create_table(
|
||||||
sa.Column('playlist_id', sa.Integer(), nullable=True),
|
"playlists",
|
||||||
sa.Column('track_id', sa.Integer(), nullable=True),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.ForeignKeyConstraint(['playlist_id'], ['playlists.id'], ),
|
sa.Column("name", sa.String(length=32), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], )
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
)
|
)
|
||||||
op.add_column('tracks', sa.Column('playdates_id', sa.Integer(), nullable=True))
|
op.create_table(
|
||||||
op.create_foreign_key(None, 'tracks', 'playdates', ['playdates_id'], ['id'])
|
"playlistracks",
|
||||||
|
sa.Column("playlist_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("track_id", sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["playlist_id"],
|
||||||
|
["playlists.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["track_id"],
|
||||||
|
["tracks.id"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.add_column("tracks", sa.Column("playdates_id", sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, "tracks", "playdates", ["playdates_id"], ["id"])
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_constraint(None, 'tracks', type_='foreignkey')
|
op.drop_constraint(None, "tracks", type_="foreignkey")
|
||||||
op.drop_column('tracks', 'playdates_id')
|
op.drop_column("tracks", "playdates_id")
|
||||||
op.drop_table('playlistracks')
|
op.drop_table("playlistracks")
|
||||||
op.drop_table('playlists')
|
op.drop_table("playlists")
|
||||||
op.drop_index(op.f('ix_playdates_lastplayed'), table_name='playdates')
|
op.drop_index(op.f("ix_playdates_lastplayed"), table_name="playdates")
|
||||||
op.drop_table('playdates')
|
op.drop_table("playdates")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
654
poetry.lock
generated
654
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,45 +5,48 @@ description = "Music player for internet radio"
|
|||||||
authors = ["Keith Edmunds <kae@midnighthax.com>"]
|
authors = ["Keith Edmunds <kae@midnighthax.com>"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.9"
|
python = "^3.11"
|
||||||
tinytag = "^1.7.0"
|
tinytag = "^1.10.1"
|
||||||
SQLAlchemy = "^2.0.22"
|
SQLAlchemy = "^2.0.29"
|
||||||
python-vlc = "^3.0.12118"
|
python-vlc = "^3.0.20123"
|
||||||
mysqlclient = "^2.1.0"
|
mysqlclient = "^2.2.4"
|
||||||
mutagen = "^1.45.1"
|
mutagen = "^1.47.0"
|
||||||
alembic = "^1.7.5"
|
alembic = "^1.13.1"
|
||||||
psutil = "^5.9.0"
|
psutil = "^5.9.8"
|
||||||
pydub = "^0.25.1"
|
pydub = "^0.25.1"
|
||||||
types-psutil = "^5.8.22"
|
types-psutil = "^5.9.5.20240316"
|
||||||
python-slugify = "^6.1.2"
|
python-slugify = "^8.0.4"
|
||||||
thefuzz = "^0.19.0"
|
thefuzz = "^0.19.0"
|
||||||
python-Levenshtein = "^0.12.2"
|
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"
|
||||||
obsws-python = "^1.4.2"
|
obsws-python = "^1.7.0"
|
||||||
pyqt6 = "^6.5.0"
|
pyqt6 = "^6.6.1"
|
||||||
pyqt6-webengine = "^6.5.0"
|
pyqt6-webengine = "^6.6.0"
|
||||||
pygame = "^2.4.0"
|
pygame = "^2.5.2"
|
||||||
pyqtgraph = "^0.13.3"
|
pyqtgraph = "^0.13.3"
|
||||||
colorlog = "^6.8.0"
|
colorlog = "^6.8.2"
|
||||||
|
alchemical = "^1.0.1"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
ipdb = "^0.13.9"
|
ipdb = "^0.13.9"
|
||||||
pytest = "^7.0.1"
|
pytest-qt = "^4.4.0"
|
||||||
pytest-qt = "^4.0.2"
|
|
||||||
pydub-stubs = "^0.25.1"
|
pydub-stubs = "^0.25.1"
|
||||||
line-profiler = "^4.0.2"
|
line-profiler = "^4.1.2"
|
||||||
flakehell = "^0.9.0"
|
flakehell = "^0.9.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pudb = "^2023.1"
|
pudb = "*"
|
||||||
sphinx = "^7.0.1"
|
sphinx = "^7.0.1"
|
||||||
furo = "^2023.5.20"
|
furo = "^2023.5.20"
|
||||||
black = "^24.2.0"
|
|
||||||
flakehell = "^0.9.0"
|
flakehell = "^0.9.0"
|
||||||
mypy = "^1.7.0"
|
mypy = "^1.7.0"
|
||||||
pdbp = "^1.5.0"
|
pdbp = "^1.5.0"
|
||||||
|
pytest-cov = "^5.0.0"
|
||||||
|
pytest = "^8.1.1"
|
||||||
|
snoop = "^0.4.3"
|
||||||
|
black = "^24.3.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
@ -56,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"
|
filterwarnings = ["ignore:'audioop' is deprecated", "ignore:pkg_resources"]
|
||||||
|
|
||||||
[tool.vulture]
|
[tool.vulture]
|
||||||
exclude = ["migrations", "app/ui", "archive"]
|
exclude = ["migrations", "app/ui", "archive"]
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from helpers import (
|
|
||||||
fade_point,
|
|
||||||
get_audio_segment,
|
|
||||||
get_tags,
|
|
||||||
get_relative_date,
|
|
||||||
leading_silence,
|
|
||||||
ms_to_mmss,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_fade_point():
|
|
||||||
test_track_path = "testdata/isa.mp3"
|
|
||||||
test_track_data = "testdata/isa.py"
|
|
||||||
|
|
||||||
audio_segment = get_audio_segment(test_track_path)
|
|
||||||
assert audio_segment
|
|
||||||
|
|
||||||
fade_at = fade_point(audio_segment)
|
|
||||||
|
|
||||||
# Get test data
|
|
||||||
with open(test_track_data) as f:
|
|
||||||
testdata = eval(f.read())
|
|
||||||
|
|
||||||
# Volume detection can vary, so ± 1 second is OK
|
|
||||||
assert fade_at < testdata["fade_at"] + 1000
|
|
||||||
assert fade_at > testdata["fade_at"] - 1000
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_tags():
|
|
||||||
test_track_path = "testdata/mom.mp3"
|
|
||||||
test_track_data = "testdata/mom.py"
|
|
||||||
|
|
||||||
tags = get_tags(test_track_path)
|
|
||||||
|
|
||||||
# Get test data
|
|
||||||
with open(test_track_data) as f:
|
|
||||||
testdata = eval(f.read())
|
|
||||||
|
|
||||||
assert tags["artist"] == testdata["artist"]
|
|
||||||
assert tags["title"] == testdata["title"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_relative_date():
|
|
||||||
assert get_relative_date(None) == "Never"
|
|
||||||
today_at_10 = datetime.now().replace(hour=10, minute=0)
|
|
||||||
today_at_11 = datetime.now().replace(hour=11, minute=0)
|
|
||||||
assert get_relative_date(today_at_10, today_at_11) == "Today 10:00"
|
|
||||||
eight_days_ago = today_at_10 - timedelta(days=8)
|
|
||||||
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
|
||||||
sixteen_days_ago = today_at_10 - timedelta(days=16)
|
|
||||||
assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
|
|
||||||
|
|
||||||
|
|
||||||
def test_leading_silence():
|
|
||||||
test_track_path = "testdata/isa.mp3"
|
|
||||||
test_track_data = "testdata/isa.py"
|
|
||||||
|
|
||||||
audio_segment = get_audio_segment(test_track_path)
|
|
||||||
assert audio_segment
|
|
||||||
|
|
||||||
silence_at = leading_silence(audio_segment)
|
|
||||||
|
|
||||||
# Get test data
|
|
||||||
with open(test_track_data) as f:
|
|
||||||
testdata = eval(f.read())
|
|
||||||
|
|
||||||
# Volume detection can vary, so ± 1 second is OK
|
|
||||||
assert silence_at < testdata["leading_silence"] + 1000
|
|
||||||
assert silence_at > testdata["leading_silence"] - 1000
|
|
||||||
|
|
||||||
|
|
||||||
def test_ms_to_mmss():
|
|
||||||
assert ms_to_mmss(None) == "-"
|
|
||||||
assert ms_to_mmss(59600) == "0:59"
|
|
||||||
assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23"
|
|
||||||
186
test_models.py
186
test_models.py
@ -1,186 +0,0 @@
|
|||||||
import os.path
|
|
||||||
|
|
||||||
import helpers
|
|
||||||
|
|
||||||
from app.models import (
|
|
||||||
NoteColours,
|
|
||||||
Playdates,
|
|
||||||
Playlists,
|
|
||||||
Tracks,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_notecolours_get_colour(session):
|
|
||||||
"""Create a colour record and retrieve all colours"""
|
|
||||||
|
|
||||||
print(">>>text_notcolours_get_colour")
|
|
||||||
note_colour = "#0bcdef"
|
|
||||||
NoteColours(session, substring="substring", colour=note_colour)
|
|
||||||
|
|
||||||
records = NoteColours.get_all(session)
|
|
||||||
assert len(records) == 1
|
|
||||||
record = records[0]
|
|
||||||
assert record.colour == note_colour
|
|
||||||
|
|
||||||
|
|
||||||
def test_notecolours_get_all(session):
|
|
||||||
"""Create two colour records and retrieve them all"""
|
|
||||||
|
|
||||||
print(">>>text_notcolours_get_all")
|
|
||||||
note1_colour = "#1bcdef"
|
|
||||||
note2_colour = "#20ff00"
|
|
||||||
NoteColours(session, substring="note1", colour=note1_colour)
|
|
||||||
NoteColours(session, substring="note2", colour=note2_colour)
|
|
||||||
|
|
||||||
records = NoteColours.get_all(session)
|
|
||||||
assert len(records) == 2
|
|
||||||
assert note1_colour in [n.colour for n in records]
|
|
||||||
assert note2_colour in [n.colour for n in records]
|
|
||||||
|
|
||||||
|
|
||||||
def test_notecolours_get_colour_none(session):
|
|
||||||
note_colour = "#3bcdef"
|
|
||||||
NoteColours(session, substring="substring", colour=note_colour)
|
|
||||||
|
|
||||||
result = NoteColours.get_colour(session, "xyz")
|
|
||||||
assert result is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_notecolours_get_colour_match(session):
|
|
||||||
note_colour = "#4bcdef"
|
|
||||||
nc = NoteColours(session, substring="sub", colour=note_colour)
|
|
||||||
assert nc
|
|
||||||
|
|
||||||
result = NoteColours.get_colour(session, "The substring")
|
|
||||||
assert result == note_colour
|
|
||||||
|
|
||||||
|
|
||||||
def test_playdates_add_playdate(session, track1):
|
|
||||||
"""Test playdate and last_played retrieval"""
|
|
||||||
|
|
||||||
playdate = Playdates(session, track1.id)
|
|
||||||
assert playdate
|
|
||||||
|
|
||||||
last_played = Playdates.last_played(session, track1.id)
|
|
||||||
assert abs((playdate.lastplayed - last_played).total_seconds()) < 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_playlist_create(session):
|
|
||||||
playlist = Playlists(session, "my playlist")
|
|
||||||
assert playlist
|
|
||||||
|
|
||||||
|
|
||||||
# def test_playlist_add_track(session, track):
|
|
||||||
# # We need a playlist
|
|
||||||
# playlist = Playlists(session, "my playlist")
|
|
||||||
|
|
||||||
# row = 17
|
|
||||||
|
|
||||||
# playlist.add_track(session, track.id, row)
|
|
||||||
|
|
||||||
# assert len(playlist.tracks) == 1
|
|
||||||
# playlist_track = playlist.tracks[row]
|
|
||||||
# assert playlist_track.path == track_path
|
|
||||||
|
|
||||||
|
|
||||||
# def test_playlist_tracks(session):
|
|
||||||
# # We need a playlist
|
|
||||||
# playlist = Playlists(session, "my playlist")
|
|
||||||
|
|
||||||
# # We need two tracks
|
|
||||||
# track1_path = "/a/b/c"
|
|
||||||
# track1_row = 17
|
|
||||||
# track1 = Tracks(session, track1_path)
|
|
||||||
|
|
||||||
# track2_path = "/x/y/z"
|
|
||||||
# track2_row = 29
|
|
||||||
# track2 = Tracks(session, track2_path)
|
|
||||||
|
|
||||||
# playlist.add_track(session, track1.id, track1_row)
|
|
||||||
# playlist.add_track(session, track2.id, track2_row)
|
|
||||||
|
|
||||||
# tracks = playlist.tracks
|
|
||||||
# assert tracks[track1_row] == track1
|
|
||||||
# assert tracks[track2_row] == track2
|
|
||||||
|
|
||||||
|
|
||||||
# def test_playlist_notes(session):
|
|
||||||
# # We need a playlist
|
|
||||||
# playlist = Playlists(session, "my playlist")
|
|
||||||
|
|
||||||
# # We need two notes
|
|
||||||
# note1_text = "note1 text"
|
|
||||||
# note1_row = 11
|
|
||||||
# _ = Notes(session, playlist.id, note1_row, note1_text)
|
|
||||||
|
|
||||||
# note2_text = "note2 text"
|
|
||||||
# note2_row = 19
|
|
||||||
# _ = Notes(session, playlist.id, note2_row, note2_text)
|
|
||||||
|
|
||||||
# notes = playlist.notes
|
|
||||||
# assert note1_text in [n.note for n in notes]
|
|
||||||
# assert note1_row in [n.row for n in notes]
|
|
||||||
# assert note2_text in [n.note for n in notes]
|
|
||||||
# assert note2_row in [n.row for n in notes]
|
|
||||||
|
|
||||||
|
|
||||||
def test_playlist_open_and_close(session):
|
|
||||||
# We need a playlist
|
|
||||||
playlist = Playlists(session, "my playlist")
|
|
||||||
|
|
||||||
assert len(Playlists.get_open(session)) == 0
|
|
||||||
assert len(Playlists.get_closed(session)) == 1
|
|
||||||
|
|
||||||
playlist.mark_open()
|
|
||||||
|
|
||||||
assert len(Playlists.get_open(session)) == 1
|
|
||||||
assert len(Playlists.get_closed(session)) == 0
|
|
||||||
|
|
||||||
playlist.close()
|
|
||||||
|
|
||||||
assert len(Playlists.get_open(session)) == 0
|
|
||||||
assert len(Playlists.get_closed(session)) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_playlist_get_all_and_by_id(session):
|
|
||||||
# We need two playlists
|
|
||||||
p1_name = "playlist one"
|
|
||||||
p2_name = "playlist two"
|
|
||||||
playlist1 = Playlists(session, p1_name)
|
|
||||||
_ = Playlists(session, p2_name)
|
|
||||||
|
|
||||||
all_playlists = Playlists.get_all(session)
|
|
||||||
assert len(all_playlists) == 2
|
|
||||||
assert p1_name in [p.name for p in all_playlists]
|
|
||||||
assert p2_name in [p.name for p in all_playlists]
|
|
||||||
assert session.get(Playlists, playlist1.id).name == p1_name
|
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_get_all_tracks(session, track1, track2):
|
|
||||||
# Need two tracks
|
|
||||||
|
|
||||||
result = [a.path for a in Tracks.get_all(session)]
|
|
||||||
assert track1.path in result
|
|
||||||
assert track2.path in result
|
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_by_path(session, track1):
|
|
||||||
|
|
||||||
assert Tracks.get_by_path(session, track1.path) is track1
|
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_by_id(session, track1):
|
|
||||||
|
|
||||||
assert session.get(Tracks, track1.id) is track1
|
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_search_artists(session, track1):
|
|
||||||
track1_artist = "Fleetwood Mac"
|
|
||||||
|
|
||||||
assert len(Tracks.search_artists(session, track1_artist)) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_tracks_search_titles(session, track1):
|
|
||||||
track1_title = "I'm So Afraid"
|
|
||||||
|
|
||||||
assert len(Tracks.search_titles(session, track1_title)) == 1
|
|
||||||
@ -1,380 +0,0 @@
|
|||||||
from pprint import pprint
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from app.models import (
|
|
||||||
Playlists,
|
|
||||||
Tracks,
|
|
||||||
)
|
|
||||||
from PyQt6.QtCore import Qt, QModelIndex
|
|
||||||
|
|
||||||
from app.helpers import get_file_metadata
|
|
||||||
from app import playlistmodel
|
|
||||||
from dbconfig import scoped_session
|
|
||||||
|
|
||||||
test_tracks = [
|
|
||||||
"testdata/isa.mp3",
|
|
||||||
"testdata/isa_with_gap.mp3",
|
|
||||||
"testdata/loser.mp3",
|
|
||||||
"testdata/lovecats-10seconds.mp3",
|
|
||||||
"testdata/lovecats.mp3",
|
|
||||||
"testdata/mom.mp3",
|
|
||||||
"testdata/sitting.mp3",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_model_with_tracks(session: scoped_session, name: Optional[str] = None) -> "playlistmodel.PlaylistModel":
|
|
||||||
playlist = Playlists(session, name or "test playlist")
|
|
||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
|
||||||
|
|
||||||
for row in range(len(test_tracks)):
|
|
||||||
track_path = test_tracks[row % len(test_tracks)]
|
|
||||||
metadata = get_file_metadata(track_path)
|
|
||||||
track = Tracks(session, **metadata)
|
|
||||||
model.insert_row(proposed_row_number=row, track_id=track.id, note=f"{row=}")
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def create_model_with_playlist_rows(
|
|
||||||
session: scoped_session, rows: int, name: Optional[str] = None
|
|
||||||
) -> "playlistmodel.PlaylistModel":
|
|
||||||
playlist = Playlists(session, name or "test playlist")
|
|
||||||
# Create a model
|
|
||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
|
||||||
for row in range(rows):
|
|
||||||
model.insert_row(proposed_row_number=row, note=str(row))
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def test_11_row_playlist(monkeypatch, session):
|
|
||||||
# Create multirow playlist
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
assert model.rowCount() == 11
|
|
||||||
assert max(model.playlist_rows.keys()) == 10
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_rows_test2(monkeypatch, session):
|
|
||||||
# move row 3 to row 5
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
model.move_rows([3], 5)
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
if row not in [3, 4, 5]:
|
|
||||||
assert model.playlist_rows[row].note == str(row)
|
|
||||||
elif row == 3:
|
|
||||||
assert model.playlist_rows[row].note == str(4)
|
|
||||||
elif row == 4:
|
|
||||||
assert model.playlist_rows[row].note == str(5)
|
|
||||||
elif row == 5:
|
|
||||||
assert model.playlist_rows[row].note == str(3)
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_rows_test3(monkeypatch, session):
|
|
||||||
# move row 4 to row 3
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
model.move_rows([4], 3)
|
|
||||||
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
if row not in [3, 4]:
|
|
||||||
assert model.playlist_rows[row].note == str(row)
|
|
||||||
elif row == 3:
|
|
||||||
assert model.playlist_rows[row].note == str(4)
|
|
||||||
elif row == 4:
|
|
||||||
assert model.playlist_rows[row].note == str(3)
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_rows_test4(monkeypatch, session):
|
|
||||||
# move row 4 to row 2
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
model.move_rows([4], 2)
|
|
||||||
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
if row not in [2, 3, 4]:
|
|
||||||
assert model.playlist_rows[row].note == str(row)
|
|
||||||
elif row == 2:
|
|
||||||
assert model.playlist_rows[row].note == str(4)
|
|
||||||
elif row == 3:
|
|
||||||
assert model.playlist_rows[row].note == str(2)
|
|
||||||
elif row == 4:
|
|
||||||
assert model.playlist_rows[row].note == str(3)
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_rows_test5(monkeypatch, session):
|
|
||||||
# move rows [1, 4, 5, 10] → 8
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
model.move_rows([1, 4, 5, 10], 8)
|
|
||||||
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
new_order = []
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
new_order.append(int(model.playlist_rows[row].note))
|
|
||||||
assert new_order == [0, 2, 3, 6, 7, 8, 9, 1, 4, 5, 10]
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_rows_test6(monkeypatch, session):
|
|
||||||
# move rows [3, 6] → 5
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
model.move_rows([3, 6], 5)
|
|
||||||
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
new_order = []
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
new_order.append(int(model.playlist_rows[row].note))
|
|
||||||
assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10]
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_rows_test7(monkeypatch, session):
|
|
||||||
# move rows [3, 5, 6] → 8
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
model.move_rows([3, 5, 6], 8)
|
|
||||||
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
new_order = []
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
new_order.append(int(model.playlist_rows[row].note))
|
|
||||||
assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6]
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_rows_test8(monkeypatch, session):
|
|
||||||
# move rows [7, 8, 10] → 5
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, 11)
|
|
||||||
model.move_rows([7, 8, 10], 5)
|
|
||||||
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
new_order = []
|
|
||||||
for row in range(model.rowCount()):
|
|
||||||
assert row in model.playlist_rows
|
|
||||||
assert model.playlist_rows[row].plr_rownum == row
|
|
||||||
new_order.append(int(model.playlist_rows[row].note))
|
|
||||||
assert new_order == [0, 1, 2, 3, 4, 7, 8, 10, 5, 6, 9]
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_header_row_end(monkeypatch, session):
|
|
||||||
# insert header row at end of playlist
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
note_text = "test text"
|
|
||||||
initial_row_count = 11
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, initial_row_count)
|
|
||||||
model.insert_row(proposed_row_number=None, note=note_text)
|
|
||||||
assert model.rowCount() == initial_row_count + 1
|
|
||||||
prd = model.playlist_rows[model.rowCount() - 1]
|
|
||||||
# Test against edit_role because display_role for headers is
|
|
||||||
# handled differently (sets up row span)
|
|
||||||
assert (
|
|
||||||
model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd)
|
|
||||||
== note_text
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_header_row_middle(monkeypatch, session):
|
|
||||||
# insert header row in middle of playlist
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
note_text = "test text"
|
|
||||||
initial_row_count = 11
|
|
||||||
insert_row = 6
|
|
||||||
|
|
||||||
model = create_model_with_playlist_rows(session, initial_row_count)
|
|
||||||
model.insert_row(proposed_row_number=insert_row, note=note_text)
|
|
||||||
assert model.rowCount() == initial_row_count + 1
|
|
||||||
prd = model.playlist_rows[insert_row]
|
|
||||||
# Test against edit_role because display_role for headers is
|
|
||||||
# handled differently (sets up row span)
|
|
||||||
assert (
|
|
||||||
model.edit_role(model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd)
|
|
||||||
== note_text
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_model_with_tracks(monkeypatch, session):
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
model = create_model_with_tracks(session)
|
|
||||||
assert len(model.playlist_rows) == len(test_tracks)
|
|
||||||
|
|
||||||
|
|
||||||
def test_timing_one_track(monkeypatch, session):
|
|
||||||
START_ROW = 0
|
|
||||||
END_ROW = 2
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
model = create_model_with_tracks(session)
|
|
||||||
|
|
||||||
model.insert_row(proposed_row_number=START_ROW, note="start+")
|
|
||||||
model.insert_row(proposed_row_number=END_ROW, note="-")
|
|
||||||
|
|
||||||
prd = model.playlist_rows[START_ROW]
|
|
||||||
qv_value = model.display_role(START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd)
|
|
||||||
assert qv_value.value() == "start [1 tracks, 4:23 unplayed]"
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_track_new_playlist(monkeypatch, session):
|
|
||||||
# insert a track into a new playlist
|
|
||||||
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
playlist = Playlists(session, "test playlist")
|
|
||||||
# Create a model
|
|
||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
|
||||||
|
|
||||||
track_path = test_tracks[0]
|
|
||||||
metadata = get_file_metadata(track_path)
|
|
||||||
track = Tracks(session, **metadata)
|
|
||||||
model.insert_row(proposed_row_number=0, track_id=track.id)
|
|
||||||
|
|
||||||
prd = model.playlist_rows[model.rowCount() - 1]
|
|
||||||
assert (
|
|
||||||
model.edit_role(model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd)
|
|
||||||
== metadata["title"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_row_groups_one_row(monkeypatch, session):
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
rows_to_move = [3]
|
|
||||||
|
|
||||||
model_src = create_model_with_playlist_rows(session, 5, name="source")
|
|
||||||
result = model_src._reversed_contiguous_row_groups(rows_to_move)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0] == [3]
|
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_row_groups_multiple_row(monkeypatch, session):
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21]
|
|
||||||
|
|
||||||
model_src = create_model_with_playlist_rows(session, 5, name="source")
|
|
||||||
result = model_src._reversed_contiguous_row_groups(rows_to_move)
|
|
||||||
|
|
||||||
assert result == [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_one_row_between_playlists_to_end(monkeypatch, session):
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
create_rowcount = 5
|
|
||||||
from_rows = [3]
|
|
||||||
to_row = create_rowcount
|
|
||||||
|
|
||||||
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
|
||||||
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
|
||||||
|
|
||||||
model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
|
|
||||||
model_dst.refresh_data(session)
|
|
||||||
|
|
||||||
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
|
||||||
assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows)
|
|
||||||
assert sorted([a.plr_rownum for a in model_src.playlist_rows.values()]) == list(
|
|
||||||
range(len(model_src.playlist_rows))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_one_row_between_playlists_to_middle(monkeypatch, session):
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
create_rowcount = 5
|
|
||||||
from_rows = [3]
|
|
||||||
to_row = 2
|
|
||||||
|
|
||||||
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
|
||||||
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
|
||||||
|
|
||||||
model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
|
|
||||||
model_dst.refresh_data(session)
|
|
||||||
|
|
||||||
# Check the rows of the destination model
|
|
||||||
row_notes = []
|
|
||||||
for row_number in range(model_dst.rowCount()):
|
|
||||||
index = model_dst.index(row_number, playlistmodel.Col.TITLE.value, QModelIndex())
|
|
||||||
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
|
|
||||||
|
|
||||||
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
|
||||||
assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows)
|
|
||||||
assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4]
|
|
||||||
|
|
||||||
|
|
||||||
def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session):
|
|
||||||
monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
|
|
||||||
create_rowcount = 5
|
|
||||||
from_rows = [1, 3, 4]
|
|
||||||
to_row = 2
|
|
||||||
|
|
||||||
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
|
||||||
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
|
||||||
|
|
||||||
model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
|
|
||||||
model_dst.refresh_data(session)
|
|
||||||
|
|
||||||
# Check the rows of the destination model
|
|
||||||
row_notes = []
|
|
||||||
for row_number in range(model_dst.rowCount()):
|
|
||||||
index = model_dst.index(row_number, playlistmodel.Col.TITLE.value, QModelIndex())
|
|
||||||
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
|
|
||||||
|
|
||||||
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
|
||||||
assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows)
|
|
||||||
assert [int(a) for a in row_notes] == [0, 1, 3, 4, 1, 2, 3, 4]
|
|
||||||
|
|
||||||
|
|
||||||
# def test_edit_header(monkeypatch, session): # edit header row in middle of playlist
|
|
||||||
|
|
||||||
# monkeypatch.setattr(playlistmodel, "Session", session)
|
|
||||||
# note_text = "test text"
|
|
||||||
# initial_row_count = 11
|
|
||||||
# insert_row = 6
|
|
||||||
|
|
||||||
# model = create_model_with_playlist_rows(session, initial_row_count)
|
|
||||||
# model.insert_header_row(insert_row, note_text)
|
|
||||||
# assert model.rowCount() == initial_row_count + 1
|
|
||||||
# prd = model.playlist_rows[insert_row]
|
|
||||||
# # Test against edit_role because display_role for headers is
|
|
||||||
# # handled differently (sets up row span)
|
|
||||||
# assert (
|
|
||||||
# model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd)
|
|
||||||
# == note_text
|
|
||||||
# )
|
|
||||||
101
tests/test_helpers.py
Normal file
101
tests/test_helpers.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import datetime as dt
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from helpers import (
|
||||||
|
fade_point,
|
||||||
|
get_audio_segment,
|
||||||
|
get_tags,
|
||||||
|
get_relative_date,
|
||||||
|
leading_silence,
|
||||||
|
ms_to_mmss,
|
||||||
|
normalise_track,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMMHelpers(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_fade_point(self):
|
||||||
|
test_track_path = "testdata/isa.mp3"
|
||||||
|
test_track_data = "testdata/isa.py"
|
||||||
|
|
||||||
|
audio_segment = get_audio_segment(test_track_path)
|
||||||
|
assert audio_segment
|
||||||
|
|
||||||
|
fade_at = fade_point(audio_segment)
|
||||||
|
|
||||||
|
# Get test data
|
||||||
|
with open(test_track_data) as f:
|
||||||
|
testdata = eval(f.read())
|
||||||
|
|
||||||
|
# Volume detection can vary, so ± 1 second is OK
|
||||||
|
assert fade_at < testdata["fade_at"] + 1000
|
||||||
|
assert fade_at > testdata["fade_at"] - 1000
|
||||||
|
|
||||||
|
def test_get_tags(self):
|
||||||
|
test_track_path = "testdata/mom.mp3"
|
||||||
|
test_track_data = "testdata/mom.py"
|
||||||
|
|
||||||
|
tags = get_tags(test_track_path)
|
||||||
|
|
||||||
|
# Get test data
|
||||||
|
with open(test_track_data) as f:
|
||||||
|
testdata = eval(f.read())
|
||||||
|
|
||||||
|
assert tags["artist"] == testdata["artist"]
|
||||||
|
assert tags["title"] == testdata["title"]
|
||||||
|
|
||||||
|
def test_get_relative_date(self):
|
||||||
|
assert get_relative_date(None) == "Never"
|
||||||
|
today_at_10 = dt.datetime.now().replace(hour=10, minute=0)
|
||||||
|
today_at_11 = dt.datetime.now().replace(hour=11, minute=0)
|
||||||
|
assert get_relative_date(today_at_10, today_at_11) == "Today 10:00"
|
||||||
|
eight_days_ago = today_at_10 - dt.timedelta(days=8)
|
||||||
|
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
|
||||||
|
sixteen_days_ago = today_at_10 - dt.timedelta(days=16)
|
||||||
|
assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
|
||||||
|
|
||||||
|
def test_leading_silence(self):
|
||||||
|
test_track_path = "testdata/isa.mp3"
|
||||||
|
test_track_data = "testdata/isa.py"
|
||||||
|
|
||||||
|
audio_segment = get_audio_segment(test_track_path)
|
||||||
|
assert audio_segment
|
||||||
|
|
||||||
|
silence_at = leading_silence(audio_segment)
|
||||||
|
|
||||||
|
# Get test data
|
||||||
|
with open(test_track_data) as f:
|
||||||
|
testdata = eval(f.read())
|
||||||
|
|
||||||
|
# Volume detection can vary, so ± 1 second is OK
|
||||||
|
assert silence_at < testdata["leading_silence"] + 1000
|
||||||
|
assert silence_at > testdata["leading_silence"] - 1000
|
||||||
|
|
||||||
|
def test_ms_to_mmss(self):
|
||||||
|
assert ms_to_mmss(None) == "-"
|
||||||
|
assert ms_to_mmss(59600) == "0:59"
|
||||||
|
assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23"
|
||||||
|
|
||||||
|
def test_normalise(self):
|
||||||
|
"""Make copies to normalise to avoid corrupting source"""
|
||||||
|
|
||||||
|
_, mp3_temp_path = tempfile.mkstemp(suffix=".mp3")
|
||||||
|
shutil.copyfile("testdata/isa.mp3", mp3_temp_path)
|
||||||
|
normalise_track(mp3_temp_path)
|
||||||
|
|
||||||
|
_, flac_temp_path = tempfile.mkstemp(suffix=".flac")
|
||||||
|
shutil.copyfile("testdata/isa.flac", flac_temp_path)
|
||||||
|
normalise_track(flac_temp_path)
|
||||||
50
tests/test_misc.py
Normal file
50
tests/test_misc.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
|
from models import db, Settings # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class TestMMMisc(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
def test_log_exception(self):
|
||||||
|
"""Test deliberate exception"""
|
||||||
|
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
def test_create_settings(self):
|
||||||
|
SETTING_NAME = "wombat"
|
||||||
|
NO_SUCH_SETTING = "abc"
|
||||||
|
VALUE = 3
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
setting = Settings(session, SETTING_NAME)
|
||||||
|
# test repr
|
||||||
|
_ = str(setting)
|
||||||
|
setting.update(session, dict(f_int=VALUE))
|
||||||
|
_ = Settings.all_as_dict(session)
|
||||||
|
test = Settings.get_int_settings(session, SETTING_NAME)
|
||||||
|
assert test.name == SETTING_NAME
|
||||||
|
assert test.f_int == VALUE
|
||||||
|
test_new = Settings.get_int_settings(session, NO_SUCH_SETTING)
|
||||||
|
assert test_new.name == NO_SUCH_SETTING
|
||||||
304
tests/test_models.py
Normal file
304
tests/test_models.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import datetime as dt
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from app import helpers
|
||||||
|
|
||||||
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
|
from app.models import ( # noqa: E402
|
||||||
|
db,
|
||||||
|
Carts,
|
||||||
|
NoteColours,
|
||||||
|
Playdates,
|
||||||
|
Playlists,
|
||||||
|
PlaylistRows,
|
||||||
|
Tracks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMMModels(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
track1_path = "testdata/isa.mp3"
|
||||||
|
metadata1 = helpers.get_file_metadata(track1_path)
|
||||||
|
self.track1 = Tracks(session, **metadata1)
|
||||||
|
|
||||||
|
track2_path = "testdata/mom.mp3"
|
||||||
|
metadata2 = helpers.get_file_metadata(track2_path)
|
||||||
|
self.track2 = Tracks(session, **metadata2)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
def test_track_repr(self):
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
_ = str(self.track1)
|
||||||
|
|
||||||
|
def test_notecolours_get_colour(self):
|
||||||
|
"""Create a colour record and retrieve all colours"""
|
||||||
|
|
||||||
|
note_colour = "#0bcdef"
|
||||||
|
with db.Session() as session:
|
||||||
|
NoteColours(session, substring="substring", colour=note_colour)
|
||||||
|
|
||||||
|
records = NoteColours.get_all(session)
|
||||||
|
assert len(records) == 1
|
||||||
|
record = records[0]
|
||||||
|
assert record.colour == note_colour
|
||||||
|
|
||||||
|
def test_notecolours_get_all(self):
|
||||||
|
"""Create two colour records and retrieve them all"""
|
||||||
|
|
||||||
|
note1_colour = "#1bcdef"
|
||||||
|
note2_colour = "#20ff00"
|
||||||
|
with db.Session() as session:
|
||||||
|
NoteColours(session, substring="note1", colour=note1_colour)
|
||||||
|
NoteColours(session, substring="note2", colour=note2_colour)
|
||||||
|
|
||||||
|
records = NoteColours.get_all(session)
|
||||||
|
assert len(records) == 2
|
||||||
|
assert note1_colour in [n.colour for n in records]
|
||||||
|
assert note2_colour in [n.colour for n in records]
|
||||||
|
|
||||||
|
def test_notecolours_get_colour_none(self):
|
||||||
|
note_colour = "#3bcdef"
|
||||||
|
with db.Session() as session:
|
||||||
|
NoteColours(session, substring="substring", colour=note_colour)
|
||||||
|
|
||||||
|
result = NoteColours.get_colour(session, "xyz")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_notecolours_get_colour_match(self):
|
||||||
|
note_colour = "#4bcdef"
|
||||||
|
with db.Session() as session:
|
||||||
|
nc = NoteColours(session, substring="sub", colour=note_colour)
|
||||||
|
assert nc
|
||||||
|
|
||||||
|
result = NoteColours.get_colour(session, "The substring")
|
||||||
|
assert result == note_colour
|
||||||
|
|
||||||
|
def test_playdates_add_playdate(self):
|
||||||
|
"""Test playdate and last_played retrieval"""
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
playdate = Playdates(session, self.track1.id)
|
||||||
|
assert playdate
|
||||||
|
# test repr
|
||||||
|
_ = str(playdate)
|
||||||
|
|
||||||
|
last_played = Playdates.last_played(session, self.track1.id)
|
||||||
|
assert abs((playdate.lastplayed - last_played).total_seconds()) < 2
|
||||||
|
|
||||||
|
def test_playdates_played_after(self):
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
playdate = Playdates(session, self.track1.id)
|
||||||
|
yesterday = dt.datetime.now() - dt.timedelta(days=1)
|
||||||
|
played = Playdates.played_after(session, yesterday)
|
||||||
|
|
||||||
|
assert len(played) == 1
|
||||||
|
assert played[0] == playdate
|
||||||
|
|
||||||
|
def test_playlist_create(self):
|
||||||
|
TEMPLATE_NAME = "my template"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
assert playlist
|
||||||
|
# test repr
|
||||||
|
_ = str(playlist)
|
||||||
|
|
||||||
|
# test clear tabs
|
||||||
|
Playlists.clear_tabs(session, [playlist.id])
|
||||||
|
|
||||||
|
# create template
|
||||||
|
Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME)
|
||||||
|
|
||||||
|
# test create template
|
||||||
|
_ = Playlists.create_playlist_from_template(
|
||||||
|
session, playlist, "my new name"
|
||||||
|
)
|
||||||
|
|
||||||
|
# get all templates
|
||||||
|
all_templates = Playlists.get_all_templates(session)
|
||||||
|
assert len(all_templates) == 1
|
||||||
|
# Save as template creates new playlist
|
||||||
|
assert all_templates[0] != playlist
|
||||||
|
# test delete playlist
|
||||||
|
playlist.delete(session)
|
||||||
|
|
||||||
|
def test_playlist_open_and_close(self):
|
||||||
|
# We need a playlist
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist = Playlists(session, "my playlist")
|
||||||
|
|
||||||
|
assert len(Playlists.get_open(session)) == 0
|
||||||
|
assert len(Playlists.get_closed(session)) == 1
|
||||||
|
|
||||||
|
playlist.mark_open()
|
||||||
|
|
||||||
|
assert len(Playlists.get_open(session)) == 1
|
||||||
|
assert len(Playlists.get_closed(session)) == 0
|
||||||
|
|
||||||
|
playlist.close()
|
||||||
|
|
||||||
|
assert len(Playlists.get_open(session)) == 0
|
||||||
|
assert len(Playlists.get_closed(session)) == 1
|
||||||
|
|
||||||
|
def test_playlist_get_all_and_by_id(self):
|
||||||
|
# We need two playlists
|
||||||
|
p1_name = "playlist one"
|
||||||
|
p2_name = "playlist two"
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist1 = Playlists(session, p1_name)
|
||||||
|
_ = Playlists(session, p2_name)
|
||||||
|
|
||||||
|
all_playlists = Playlists.get_all(session)
|
||||||
|
assert len(all_playlists) == 2
|
||||||
|
assert p1_name in [p.name for p in all_playlists]
|
||||||
|
assert p2_name in [p.name for p in all_playlists]
|
||||||
|
assert session.get(Playlists, playlist1.id).name == p1_name
|
||||||
|
|
||||||
|
def test_tracks_get_all_tracks(self):
|
||||||
|
# Need two tracks
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
session.add(self.track2)
|
||||||
|
result = [a.path for a in Tracks.get_all(session)]
|
||||||
|
assert self.track1.path in result
|
||||||
|
assert self.track2.path in result
|
||||||
|
|
||||||
|
def test_tracks_by_path(self):
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
assert Tracks.get_by_path(session, self.track1.path) is self.track1
|
||||||
|
|
||||||
|
def test_tracks_by_id(self):
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
assert session.get(Tracks, self.track1.id) is self.track1
|
||||||
|
|
||||||
|
def test_tracks_search_artists(self):
|
||||||
|
track1_artist = "Fleetwood Mac"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
assert len(Tracks.search_artists(session, track1_artist)) == 1
|
||||||
|
|
||||||
|
def test_tracks_search_titles(self):
|
||||||
|
track1_title = "I'm So Afraid"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
session.add(self.track1)
|
||||||
|
assert len(Tracks.search_titles(session, track1_title)) == 1
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
"""Just check for error retrieving reprs"""
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
nc = NoteColours(session, substring="x", colour="x")
|
||||||
|
_ = str(nc)
|
||||||
|
|
||||||
|
def test_get_colour(self):
|
||||||
|
"""Test for errors in execution"""
|
||||||
|
|
||||||
|
GOOD_STRING = "cantelope"
|
||||||
|
BAD_STRING = "ericTheBee"
|
||||||
|
SUBSTR = "ant"
|
||||||
|
COLOUR = "blue"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
nc1 = NoteColours(
|
||||||
|
session, substring=SUBSTR, colour=COLOUR, is_casesensitive=True
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = nc1.get_colour(session, "")
|
||||||
|
colour = nc1.get_colour(session, GOOD_STRING)
|
||||||
|
assert colour == COLOUR
|
||||||
|
|
||||||
|
colour = nc1.get_colour(session, BAD_STRING)
|
||||||
|
assert colour is None
|
||||||
|
|
||||||
|
nc2 = NoteColours(
|
||||||
|
session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True
|
||||||
|
)
|
||||||
|
colour = nc2.get_colour(session, GOOD_STRING)
|
||||||
|
assert colour == COLOUR
|
||||||
|
|
||||||
|
colour = nc2.get_colour(session, BAD_STRING)
|
||||||
|
assert colour is None
|
||||||
|
|
||||||
|
nc3 = NoteColours(
|
||||||
|
session,
|
||||||
|
substring=".*" + SUBSTR,
|
||||||
|
colour=COLOUR,
|
||||||
|
is_regex=True,
|
||||||
|
is_casesensitive=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
colour = nc3.get_colour(session, GOOD_STRING)
|
||||||
|
assert colour == COLOUR
|
||||||
|
|
||||||
|
colour = nc3.get_colour(session, BAD_STRING)
|
||||||
|
assert colour is None
|
||||||
|
|
||||||
|
def test_create_cart(self):
|
||||||
|
with db.Session() as session:
|
||||||
|
cart = Carts(session, 1, "name")
|
||||||
|
assert cart
|
||||||
|
_ = str(cart)
|
||||||
|
|
||||||
|
def test_name_available(self):
|
||||||
|
PLAYLIST_NAME = "a name"
|
||||||
|
RENAME = "new name"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
||||||
|
playlist = Playlists(session, PLAYLIST_NAME)
|
||||||
|
assert playlist
|
||||||
|
|
||||||
|
assert Playlists.name_is_available(session, PLAYLIST_NAME) is False
|
||||||
|
|
||||||
|
playlist.rename(session, RENAME)
|
||||||
|
|
||||||
|
def test_create_playlist_row(self):
|
||||||
|
PLAYLIST_NAME = "a name"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
||||||
|
playlist = Playlists(session, PLAYLIST_NAME)
|
||||||
|
|
||||||
|
plr = PlaylistRows(session, playlist.id, 1)
|
||||||
|
assert plr
|
||||||
|
_ = str(plr)
|
||||||
|
plr.append_note("a note")
|
||||||
|
plr.append_note("another note")
|
||||||
|
|
||||||
|
def test_delete_plr(self):
|
||||||
|
PLAYLIST_NAME = "a name"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
||||||
|
playlist = Playlists(session, PLAYLIST_NAME)
|
||||||
|
|
||||||
|
plr = PlaylistRows(session, playlist.id, 1)
|
||||||
|
assert plr
|
||||||
|
PlaylistRows.delete_higher_rows(session, plr.playlist_id, 10)
|
||||||
|
|
||||||
|
assert PlaylistRows.get_track_plr(session, 12, plr.playlist_id) is None
|
||||||
420
tests/test_playlistmodel.py
Normal file
420
tests/test_playlistmodel.py
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
from PyQt6.QtCore import Qt, QModelIndex
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from app.log import log
|
||||||
|
from app.helpers import get_file_metadata
|
||||||
|
|
||||||
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
|
from app import playlistmodel # noqa: E402
|
||||||
|
from app.models import ( # noqa: E402
|
||||||
|
db,
|
||||||
|
Playlists,
|
||||||
|
Settings,
|
||||||
|
Tracks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMMMiscTracks(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
PLAYLIST_NAME = "tracks playlist"
|
||||||
|
self.test_tracks = [
|
||||||
|
"testdata/isa.mp3",
|
||||||
|
"testdata/isa_with_gap.mp3",
|
||||||
|
"testdata/loser.mp3",
|
||||||
|
"testdata/lovecats-10seconds.mp3",
|
||||||
|
"testdata/lovecats.mp3",
|
||||||
|
"testdata/mom.mp3",
|
||||||
|
"testdata/sitting.mp3",
|
||||||
|
"testdata/wrb.flac",
|
||||||
|
]
|
||||||
|
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Create a playlist and model
|
||||||
|
with db.Session() as session:
|
||||||
|
self.playlist = Playlists(session, PLAYLIST_NAME)
|
||||||
|
self.model = playlistmodel.PlaylistModel(self.playlist.id)
|
||||||
|
|
||||||
|
for row in range(len(self.test_tracks)):
|
||||||
|
track_path = self.test_tracks[row % len(self.test_tracks)]
|
||||||
|
metadata = get_file_metadata(track_path)
|
||||||
|
track = Tracks(session, **metadata)
|
||||||
|
self.model.insert_row(
|
||||||
|
proposed_row_number=row, track_id=track.id, note=f"{row=}"
|
||||||
|
)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
def test_8_row_playlist(self):
|
||||||
|
# Test auto-created playlist
|
||||||
|
|
||||||
|
assert self.model.rowCount() == 8
|
||||||
|
assert max(self.model.playlist_rows.keys()) == 7
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
|
||||||
|
def test_timing_one_track(self):
|
||||||
|
START_ROW = 0
|
||||||
|
END_ROW = 2
|
||||||
|
|
||||||
|
self.model.insert_row(proposed_row_number=START_ROW, note="start+")
|
||||||
|
self.model.insert_row(proposed_row_number=END_ROW, note="-")
|
||||||
|
|
||||||
|
prd = self.model.playlist_rows[START_ROW]
|
||||||
|
qv_value = self.model.display_role(
|
||||||
|
START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd
|
||||||
|
)
|
||||||
|
assert qv_value.value() == "start [1 tracks, 4:23 unplayed]"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMMMiscNoPlaylist(unittest.TestCase):
|
||||||
|
PLAYLIST_NAME = "tracks playlist"
|
||||||
|
test_tracks = [
|
||||||
|
"testdata/isa.mp3",
|
||||||
|
"testdata/isa_with_gap.mp3",
|
||||||
|
"testdata/loser.mp3",
|
||||||
|
"testdata/lovecats-10seconds.mp3",
|
||||||
|
"testdata/lovecats.mp3",
|
||||||
|
"testdata/mom.mp3",
|
||||||
|
"testdata/sitting.mp3",
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
def test_insert_track_new_playlist(self):
|
||||||
|
# insert a track into a new playlist
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist = Playlists(session, self.PLAYLIST_NAME)
|
||||||
|
# Create a model
|
||||||
|
model = playlistmodel.PlaylistModel(playlist.id)
|
||||||
|
# test repr
|
||||||
|
_ = str(model)
|
||||||
|
|
||||||
|
track_path = self.test_tracks[0]
|
||||||
|
metadata = get_file_metadata(track_path)
|
||||||
|
track = Tracks(session, **metadata)
|
||||||
|
model.insert_row(proposed_row_number=0, track_id=track.id)
|
||||||
|
|
||||||
|
prd = model.playlist_rows[model.rowCount() - 1]
|
||||||
|
# test repr
|
||||||
|
_ = str(prd)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
model.edit_role(
|
||||||
|
model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd
|
||||||
|
)
|
||||||
|
== metadata["title"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMMMiscRowMove(unittest.TestCase):
|
||||||
|
PLAYLIST_NAME = "rowmove playlist"
|
||||||
|
ROWS_TO_CREATE = 11
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
self.playlist = Playlists(session, self.PLAYLIST_NAME)
|
||||||
|
self.model = playlistmodel.PlaylistModel(self.playlist.id)
|
||||||
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
|
self.model.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
def test_move_rows_test2(self):
|
||||||
|
# move row 3 to row 5
|
||||||
|
self.model.move_rows([3], 5)
|
||||||
|
# Check we have all rows and plr_rownums are correct
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
if row not in [3, 4, 5]:
|
||||||
|
assert self.model.playlist_rows[row].note == str(row)
|
||||||
|
elif row == 3:
|
||||||
|
assert self.model.playlist_rows[row].note == str(4)
|
||||||
|
elif row == 4:
|
||||||
|
assert self.model.playlist_rows[row].note == str(5)
|
||||||
|
elif row == 5:
|
||||||
|
assert self.model.playlist_rows[row].note == str(3)
|
||||||
|
|
||||||
|
def test_move_rows_test3(self):
|
||||||
|
# move row 4 to row 3
|
||||||
|
|
||||||
|
self.model.move_rows([4], 3)
|
||||||
|
|
||||||
|
# Check we have all rows and plr_rownums are correct
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
if row not in [3, 4]:
|
||||||
|
assert self.model.playlist_rows[row].note == str(row)
|
||||||
|
elif row == 3:
|
||||||
|
assert self.model.playlist_rows[row].note == str(4)
|
||||||
|
elif row == 4:
|
||||||
|
assert self.model.playlist_rows[row].note == str(3)
|
||||||
|
|
||||||
|
def test_move_rows_test4(self):
|
||||||
|
# move row 4 to row 2
|
||||||
|
|
||||||
|
self.model.move_rows([4], 2)
|
||||||
|
|
||||||
|
# Check we have all rows and plr_rownums are correct
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
if row not in [2, 3, 4]:
|
||||||
|
assert self.model.playlist_rows[row].note == str(row)
|
||||||
|
elif row == 2:
|
||||||
|
assert self.model.playlist_rows[row].note == str(4)
|
||||||
|
elif row == 3:
|
||||||
|
assert self.model.playlist_rows[row].note == str(2)
|
||||||
|
elif row == 4:
|
||||||
|
assert self.model.playlist_rows[row].note == str(3)
|
||||||
|
|
||||||
|
def test_move_rows_test5(self):
|
||||||
|
# move rows [1, 4, 5, 10] → 8
|
||||||
|
|
||||||
|
self.model.move_rows([1, 4, 5, 10], 8)
|
||||||
|
|
||||||
|
# Check we have all rows and plr_rownums are correct
|
||||||
|
new_order = []
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
new_order.append(int(self.model.playlist_rows[row].note))
|
||||||
|
assert new_order == [0, 2, 3, 6, 7, 8, 9, 1, 4, 5, 10]
|
||||||
|
|
||||||
|
def test_move_rows_test6(self):
|
||||||
|
# move rows [3, 6] → 5
|
||||||
|
|
||||||
|
self.model.move_rows([3, 6], 5)
|
||||||
|
|
||||||
|
# Check we have all rows and plr_rownums are correct
|
||||||
|
new_order = []
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
new_order.append(int(self.model.playlist_rows[row].note))
|
||||||
|
assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
|
def test_move_rows_test7(self):
|
||||||
|
# move rows [3, 5, 6] → 8
|
||||||
|
|
||||||
|
self.model.move_rows([3, 5, 6], 8)
|
||||||
|
|
||||||
|
# Check we have all rows and plr_rownums are correct
|
||||||
|
new_order = []
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
new_order.append(int(self.model.playlist_rows[row].note))
|
||||||
|
assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6]
|
||||||
|
|
||||||
|
def test_move_rows_test8(self):
|
||||||
|
# move rows [7, 8, 10] → 5
|
||||||
|
|
||||||
|
self.model.move_rows([7, 8, 10], 5)
|
||||||
|
|
||||||
|
# Check we have all rows and plr_rownums are correct
|
||||||
|
new_order = []
|
||||||
|
for row in range(self.model.rowCount()):
|
||||||
|
assert row in self.model.playlist_rows
|
||||||
|
assert self.model.playlist_rows[row].plr_rownum == row
|
||||||
|
new_order.append(int(self.model.playlist_rows[row].note))
|
||||||
|
assert new_order == [0, 1, 2, 3, 4, 7, 8, 10, 5, 6, 9]
|
||||||
|
|
||||||
|
def test_insert_header_row_end(self):
|
||||||
|
# insert header row at end of playlist
|
||||||
|
|
||||||
|
note_text = "test text"
|
||||||
|
|
||||||
|
assert self.model.rowCount() == self.ROWS_TO_CREATE
|
||||||
|
self.model.insert_row(proposed_row_number=None, note=note_text)
|
||||||
|
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
|
||||||
|
prd = self.model.playlist_rows[self.model.rowCount() - 1]
|
||||||
|
# Test against edit_role because display_role for headers is
|
||||||
|
# handled differently (sets up row span)
|
||||||
|
assert (
|
||||||
|
self.model.edit_role(
|
||||||
|
self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd
|
||||||
|
)
|
||||||
|
== note_text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_insert_header_row_middle(self):
|
||||||
|
# insert header row in middle of playlist
|
||||||
|
|
||||||
|
note_text = "test text"
|
||||||
|
insert_row = 6
|
||||||
|
|
||||||
|
self.model.insert_row(proposed_row_number=insert_row, note=note_text)
|
||||||
|
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
|
||||||
|
prd = self.model.playlist_rows[insert_row]
|
||||||
|
# Test against edit_role because display_role for headers is
|
||||||
|
# handled differently (sets up row span)
|
||||||
|
assert (
|
||||||
|
self.model.edit_role(
|
||||||
|
self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd
|
||||||
|
)
|
||||||
|
== note_text
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_add_track_to_header(self):
|
||||||
|
note_text = "test text"
|
||||||
|
insert_row = 6
|
||||||
|
|
||||||
|
self.model.insert_row(proposed_row_number=insert_row, note=note_text)
|
||||||
|
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
|
||||||
|
|
||||||
|
prd = self.model.playlist_rows[1]
|
||||||
|
self.model.add_track_to_header(insert_row, prd.track_id)
|
||||||
|
|
||||||
|
def test_reverse_row_groups_one_row(self):
|
||||||
|
rows_to_move = [3]
|
||||||
|
|
||||||
|
result = self.model._reversed_contiguous_row_groups(rows_to_move)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == [3]
|
||||||
|
|
||||||
|
def test_reverse_row_groups_multiple_row(self):
|
||||||
|
rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21]
|
||||||
|
|
||||||
|
result = self.model._reversed_contiguous_row_groups(rows_to_move)
|
||||||
|
|
||||||
|
assert result == [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
||||||
|
|
||||||
|
def test_move_one_row_between_playlists_to_end(self):
|
||||||
|
from_rows = [3]
|
||||||
|
to_row = self.ROWS_TO_CREATE
|
||||||
|
destination_playlist = "destination"
|
||||||
|
|
||||||
|
model_src = self.model
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist_dst = Playlists(session, destination_playlist)
|
||||||
|
model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
|
||||||
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
|
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
||||||
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
|
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 sorted([a.plr_rownum for a in model_src.playlist_rows.values()]) == list(
|
||||||
|
range(len(model_src.playlist_rows))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_move_one_row_between_playlists_to_middle(self):
|
||||||
|
from_rows = [3]
|
||||||
|
to_row = 2
|
||||||
|
destination_playlist = "destination"
|
||||||
|
|
||||||
|
model_src = self.model
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist_dst = Playlists(session, destination_playlist)
|
||||||
|
model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
|
||||||
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
|
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
||||||
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
|
# Check the rows of the destination model
|
||||||
|
row_notes = []
|
||||||
|
for row_number in range(model_dst.rowCount()):
|
||||||
|
index = model_dst.index(
|
||||||
|
row_number, playlistmodel.Col.TITLE.value, QModelIndex()
|
||||||
|
)
|
||||||
|
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
|
||||||
|
|
||||||
|
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 [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):
|
||||||
|
from_rows = [1, 3, 4]
|
||||||
|
to_row = 2
|
||||||
|
destination_playlist = "destination"
|
||||||
|
|
||||||
|
model_src = self.model
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist_dst = Playlists(session, destination_playlist)
|
||||||
|
model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
|
||||||
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
|
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
||||||
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
|
# Check the rows of the destination model
|
||||||
|
row_notes = []
|
||||||
|
for row_number in range(model_dst.rowCount()):
|
||||||
|
index = model_dst.index(
|
||||||
|
row_number, playlistmodel.Col.TITLE.value, QModelIndex()
|
||||||
|
)
|
||||||
|
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
|
||||||
|
|
||||||
|
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 [int(a) for a in row_notes] == [
|
||||||
|
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
|
||||||
|
|
||||||
|
# # monkeypatch.setattr(playlistmodel, "Session", session)
|
||||||
|
# # note_text = "test text"
|
||||||
|
# # initial_row_count = 11
|
||||||
|
# # insert_row = 6
|
||||||
|
|
||||||
|
# # model = create_model_with_playlist_rows(session, initial_row_count)
|
||||||
|
# # model.insert_header_row(insert_row, note_text)
|
||||||
|
# # assert model.rowCount() == initial_row_count + 1
|
||||||
|
# # prd = model.playlist_rows[insert_row]
|
||||||
|
# # # Test against edit_role because display_role for headers is
|
||||||
|
# # # handled differently (sets up row span)
|
||||||
|
# # assert (
|
||||||
|
# # model.edit_role(model.rowCount(), playlistmodel.Col.NOTE.value, prd)
|
||||||
|
# # == note_text
|
||||||
|
# # )
|
||||||
360
tests/test_playlists.py
Normal file
360
tests/test_playlists.py
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
import pytest
|
||||||
|
from pytestqt.plugin import QtBot # type: ignore
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from app import helpers
|
||||||
|
|
||||||
|
# Set up test database before importing db
|
||||||
|
# Mark subsequent lines to ignore E402, imports not at top of file
|
||||||
|
DB_FILE = "/tmp/mm.db"
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
os.unlink(DB_FILE)
|
||||||
|
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
|
||||||
|
from app import playlistmodel, utilities
|
||||||
|
from app.models import ( # noqa: E402
|
||||||
|
db,
|
||||||
|
Carts,
|
||||||
|
NoteColours,
|
||||||
|
Playdates,
|
||||||
|
Playlists,
|
||||||
|
PlaylistRows,
|
||||||
|
Tracks,
|
||||||
|
)
|
||||||
|
from app import playlists
|
||||||
|
from app import musicmuster
|
||||||
|
|
||||||
|
|
||||||
|
# Custom fixture to adapt qtbot for use with unittest.TestCase
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def qtbot_adapter(qapp, request):
|
||||||
|
"""Adapt qtbot fixture for usefixtures and unittest.TestCase"""
|
||||||
|
request.cls.qtbot = QtBot(request)
|
||||||
|
|
||||||
|
|
||||||
|
# Wrapper to handle setup/teardown operations
|
||||||
|
def with_updown(function):
|
||||||
|
def test_wrapper(self, *args, **kwargs):
|
||||||
|
if callable(getattr(self, "up", None)):
|
||||||
|
self.up()
|
||||||
|
try:
|
||||||
|
function(self, *args, **kwargs)
|
||||||
|
finally:
|
||||||
|
if callable(getattr(self, "down", None)):
|
||||||
|
self.down()
|
||||||
|
|
||||||
|
test_wrapper.__doc__ = function.__doc__
|
||||||
|
return test_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# Apply the custom fixture to the test class
|
||||||
|
@pytest.mark.usefixtures("qtbot_adapter")
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
def up(self):
|
||||||
|
db.create_all()
|
||||||
|
self.widget = musicmuster.Window()
|
||||||
|
# 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):
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
@with_updown
|
||||||
|
def test_init(self):
|
||||||
|
"""Just check we can create a playlist_tab"""
|
||||||
|
|
||||||
|
playlist_name = "test_init playlist"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist = Playlists(session, playlist_name)
|
||||||
|
self.widget.create_playlist_tab(playlist)
|
||||||
|
with self.qtbot.waitExposed(self.widget):
|
||||||
|
self.widget.show()
|
||||||
|
|
||||||
|
@with_updown
|
||||||
|
def test_save_and_restore(self):
|
||||||
|
"""Playlist with one track, one note, save and restore"""
|
||||||
|
|
||||||
|
note_text = "my note"
|
||||||
|
playlist_name = "test_save_and_restore playlist"
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
playlist = Playlists(session, playlist_name)
|
||||||
|
model = playlistmodel.PlaylistModel(playlist.id)
|
||||||
|
|
||||||
|
# Add a track with a note
|
||||||
|
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_utilities(self):
|
||||||
|
"""Test check_db utility"""
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
Config.ROOT = os.path.join(os.path.dirname(__file__), 'testdata')
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
utilities.check_db(session)
|
||||||
|
utilities.update_bitrates(session)
|
||||||
|
|
||||||
|
# def test_meta_all_clear(qtbot, session):
|
||||||
|
# # Create playlist
|
||||||
|
# playlist = models.Playlists(session, "my playlist")
|
||||||
|
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
|
# # Add some tracks
|
||||||
|
# # Need to commit session after each one so that new row is found
|
||||||
|
# # for subsequent inserts
|
||||||
|
# track1_path = "/a/b/c"
|
||||||
|
# track1 = models.Tracks(session, track1_path)
|
||||||
|
# playlist_tab.insert_track(session, track1)
|
||||||
|
# session.commit()
|
||||||
|
# track2_path = "/d/e/f"
|
||||||
|
# track2 = models.Tracks(session, track2_path)
|
||||||
|
# playlist_tab.insert_track(session, track2)
|
||||||
|
# session.commit()
|
||||||
|
# track3_path = "/h/i/j"
|
||||||
|
# track3 = models.Tracks(session, track3_path)
|
||||||
|
# playlist_tab.insert_track(session, track3)
|
||||||
|
# session.commit()
|
||||||
|
|
||||||
|
# assert playlist_tab._get_current_track_row() is None
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
# assert playlist_tab._get_notes_rows() == []
|
||||||
|
# assert playlist_tab._get_played_track_rows() == []
|
||||||
|
# assert len(playlist_tab._get_unreadable_track_rows()) == 3
|
||||||
|
|
||||||
|
|
||||||
|
# def test_meta(qtbot, session):
|
||||||
|
# # Create playlist
|
||||||
|
# playlist = playlists.Playlists(session, "my playlist")
|
||||||
|
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
|
# # Add some tracks
|
||||||
|
# track1_path = "/a/b/c"
|
||||||
|
# track1 = models.Tracks(session, track1_path)
|
||||||
|
# playlist_tab.insert_track(session, track1)
|
||||||
|
# session.commit()
|
||||||
|
# track2_path = "/d/e/f"
|
||||||
|
# track2 = models.Tracks(session, track2_path)
|
||||||
|
# playlist_tab.insert_track(session, track2)
|
||||||
|
# session.commit()
|
||||||
|
# track3_path = "/h/i/j"
|
||||||
|
# track3 = models.Tracks(session, track3_path)
|
||||||
|
# playlist_tab.insert_track(session, track3)
|
||||||
|
# session.commit()
|
||||||
|
|
||||||
|
# assert len(playlist_tab._get_unreadable_track_rows()) == 3
|
||||||
|
|
||||||
|
# assert playlist_tab._get_played_track_rows() == []
|
||||||
|
# assert playlist_tab._get_current_track_row() is None
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
# assert playlist_tab._get_notes_rows() == []
|
||||||
|
|
||||||
|
# playlist_tab._set_played_row(0)
|
||||||
|
# assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
# assert playlist_tab._get_current_track_row() is None
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
# assert playlist_tab._get_notes_rows() == []
|
||||||
|
|
||||||
|
# # Add a note
|
||||||
|
# note_text = "my note"
|
||||||
|
# note_row = 7 # will be added as row 3
|
||||||
|
# note = models.Notes(session, playlist.id, note_row, note_text)
|
||||||
|
# playlist_tab._insert_note(session, note)
|
||||||
|
|
||||||
|
# assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
# assert playlist_tab._get_current_track_row() is None
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
# assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
# playlist_tab._set_next_track_row(1)
|
||||||
|
# assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
# assert playlist_tab._get_current_track_row() is None
|
||||||
|
# assert playlist_tab._get_next_track_row() == 1
|
||||||
|
# assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
# playlist_tab._set_current_track_row(2)
|
||||||
|
# assert playlist_tab._get_played_track_rows() == [0]
|
||||||
|
# assert playlist_tab._get_current_track_row() == 2
|
||||||
|
# assert playlist_tab._get_next_track_row() == 1
|
||||||
|
# assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
# playlist_tab._clear_played_row_status(0)
|
||||||
|
# assert playlist_tab._get_played_track_rows() == []
|
||||||
|
# assert playlist_tab._get_current_track_row() == 2
|
||||||
|
# assert playlist_tab._get_next_track_row() == 1
|
||||||
|
# assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
# playlist_tab._meta_clear_next()
|
||||||
|
# assert playlist_tab._get_played_track_rows() == []
|
||||||
|
# assert playlist_tab._get_current_track_row() == 2
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
# assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
# playlist_tab._clear_current_track_row()
|
||||||
|
# assert playlist_tab._get_played_track_rows() == []
|
||||||
|
# assert playlist_tab._get_current_track_row() is None
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
# assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
# # Test clearing again has no effect
|
||||||
|
# playlist_tab._clear_current_track_row()
|
||||||
|
# assert playlist_tab._get_played_track_rows() == []
|
||||||
|
# assert playlist_tab._get_current_track_row() is None
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
# assert playlist_tab._get_notes_rows() == [3]
|
||||||
|
|
||||||
|
|
||||||
|
# def test_clear_next(qtbot, session):
|
||||||
|
# # Create playlist
|
||||||
|
# playlist = models.Playlists(session, "my playlist")
|
||||||
|
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
|
# # Add some tracks
|
||||||
|
# track1_path = "/a/b/c"
|
||||||
|
# track1 = models.Tracks(session, track1_path)
|
||||||
|
# playlist_tab.insert_track(session, track1)
|
||||||
|
# session.commit()
|
||||||
|
# track2_path = "/d/e/f"
|
||||||
|
# track2 = models.Tracks(session, track2_path)
|
||||||
|
# playlist_tab.insert_track(session, track2)
|
||||||
|
# session.commit()
|
||||||
|
|
||||||
|
# playlist_tab._set_next_track_row(1)
|
||||||
|
# assert playlist_tab._get_next_track_row() == 1
|
||||||
|
|
||||||
|
# playlist_tab.clear_next(session)
|
||||||
|
# assert playlist_tab._get_next_track_row() is None
|
||||||
|
|
||||||
|
|
||||||
|
# def test_get_selected_row(qtbot, monkeypatch, session):
|
||||||
|
# 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 = models.Tracks(session, track1_path)
|
||||||
|
# playlist_tab.insert_track(session, track1)
|
||||||
|
# session.commit()
|
||||||
|
# track2_path = "/d/e/f"
|
||||||
|
# track2 = models.Tracks(session, track2_path)
|
||||||
|
# playlist_tab.insert_track(session, track2)
|
||||||
|
# session.commit()
|
||||||
|
|
||||||
|
# qtbot.addWidget(playlist_tab)
|
||||||
|
# with qtbot.waitExposed(window):
|
||||||
|
# window.show()
|
||||||
|
# row0_item0 = playlist_tab.item(0, 0)
|
||||||
|
# assert row0_item0 is not None
|
||||||
|
# rect = playlist_tab.visualItemRect(row0_item0)
|
||||||
|
# qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
|
||||||
|
# row_number = playlist_tab.get_selected_row()
|
||||||
|
# assert row_number == 0
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# 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 = models.Tracks.get_by_filename(session, "isa.mp3")
|
||||||
|
# track1_title = track1.title
|
||||||
|
# assert track1_title
|
||||||
|
|
||||||
|
# playlist_tab.insert_track(session, track1)
|
||||||
|
# session.commit()
|
||||||
|
# track2 = models.Tracks.get_by_filename(session, "mom.mp3")
|
||||||
|
# playlist_tab.insert_track(session, track2)
|
||||||
|
|
||||||
|
# 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())
|
||||||
|
# selected_title = playlist_tab.get_selected_title()
|
||||||
|
# assert selected_title == track1_title
|
||||||
|
|
||||||
|
# qtbot.keyPress(playlist_tab.viewport(), "N", modifier=Qt.ControlModifier)
|
||||||
|
# qtbot.wait(1000)
|
||||||
|
|
||||||
|
|
||||||
|
# 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)
|
||||||
Loading…
Reference in New Issue
Block a user