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
|
||||
if [ $(pwd) == /home/kae/mm ]; then
|
||||
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
|
||||
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
|
||||
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"
|
||||
fi
|
||||
|
||||
@ -53,7 +53,6 @@ class MyTableWidget(QTableView):
|
||||
|
||||
|
||||
class MyModel(QAbstractTableModel):
|
||||
|
||||
def columnCount(self, index):
|
||||
return 2
|
||||
|
||||
@ -71,7 +70,11 @@ class MyModel(QAbstractTableModel):
|
||||
return QVariant()
|
||||
|
||||
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):
|
||||
|
||||
@ -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 datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
import datetime as dt
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import pyqtSignal, QObject, QThread
|
||||
|
||||
# Third party imports
|
||||
import numpy as np
|
||||
import pyqtgraph as pg # type: ignore
|
||||
from sqlalchemy.orm import scoped_session
|
||||
|
||||
# App imports
|
||||
from config import Config
|
||||
from dbconfig import scoped_session
|
||||
from models import PlaylistRows
|
||||
import helpers
|
||||
|
||||
@ -108,9 +113,10 @@ class PlaylistTrack:
|
||||
|
||||
self.artist: Optional[str] = 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_graph: Optional[FadeCurve] = None
|
||||
self.fade_graph_start_updates: Optional[dt.datetime] = None
|
||||
self.fade_length: Optional[int] = None
|
||||
self.path: Optional[str] = None
|
||||
self.playlist_id: Optional[int] = None
|
||||
@ -119,7 +125,7 @@ class PlaylistTrack:
|
||||
self.resume_marker: Optional[float] = None
|
||||
self.silence_at: 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.track_id: Optional[int] = None
|
||||
|
||||
@ -177,9 +183,19 @@ class PlaylistTrack:
|
||||
Called when track starts playing
|
||||
"""
|
||||
|
||||
self.start_time = datetime.now()
|
||||
now = dt.datetime.now()
|
||||
self.start_time = now
|
||||
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):
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import datetime
|
||||
import datetime as dt
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
@ -35,10 +35,10 @@ class Config(object):
|
||||
COLOUR_WARNING_TIMER = "#ffc107"
|
||||
DBFS_SILENCE = -50
|
||||
DEBUG_FUNCTIONS: List[Optional[str]] = []
|
||||
DEBUG_MODULES: List[Optional[str]] = ["dbconfig"]
|
||||
DEBUG_MODULES: List[Optional[str]] = []
|
||||
DEFAULT_COLUMN_WIDTH = 200
|
||||
DISPLAY_SQL = False
|
||||
EPOCH = datetime.datetime(1970, 1, 1)
|
||||
EPOCH = dt.datetime(1970, 1, 1)
|
||||
ERRORS_FROM = ["noreply@midnighthax.com"]
|
||||
ERRORS_TO = ["kae@midnighthax.com"]
|
||||
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
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import QEvent, Qt
|
||||
from PyQt6.QtWidgets import QDialog, QListWidgetItem
|
||||
|
||||
# Third party imports
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
# App imports
|
||||
from classes import MusicMusterSignals
|
||||
from dbconfig import scoped_session
|
||||
from helpers import (
|
||||
ask_yes_no,
|
||||
get_relative_date,
|
||||
@ -21,7 +26,7 @@ class TrackSelectDialog(QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
session: Session,
|
||||
new_row_number: int,
|
||||
source_model: PlaylistModel,
|
||||
add_to_header: Optional[bool] = False,
|
||||
@ -104,7 +109,9 @@ class TrackSelectDialog(QDialog):
|
||||
|
||||
if self.add_to_header:
|
||||
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:
|
||||
self.source_model.add_track_to_header(self.new_row_number, track_id)
|
||||
# Close dialog - we can only add one track to a header
|
||||
@ -112,7 +119,9 @@ class TrackSelectDialog(QDialog):
|
||||
else:
|
||||
# Adding a new track row
|
||||
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:
|
||||
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 typing import Any, Dict, Optional
|
||||
import functools
|
||||
@ -99,7 +99,7 @@ def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
||||
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"""
|
||||
|
||||
try:
|
||||
@ -110,7 +110,7 @@ def get_embedded_time(text: str) -> Optional[datetime]:
|
||||
return None
|
||||
|
||||
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:
|
||||
return None
|
||||
|
||||
@ -143,7 +143,7 @@ def get_file_metadata(filepath: str) -> dict:
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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:
|
||||
return "Never"
|
||||
if not reference_date:
|
||||
reference_date = datetime.now()
|
||||
reference_date = dt.datetime.now()
|
||||
|
||||
# Check parameters
|
||||
if past_date > reference_date:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import urllib.parse
|
||||
|
||||
from datetime import datetime
|
||||
import datetime as dt
|
||||
from slugify import slugify # type: ignore
|
||||
from typing import Dict
|
||||
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_wikipedia_signal.connect(self.open_in_wikipedia)
|
||||
# re-use the oldest one later)
|
||||
self.last_update: Dict[QWebEngineView, datetime] = {}
|
||||
self.last_update: Dict[QWebEngineView, dt.datetime] = {}
|
||||
self.tabtitles: Dict[int, str] = {}
|
||||
|
||||
# Create one tab which (for some reason) creates flickering if
|
||||
# done later
|
||||
widget = QWebEngineView()
|
||||
widget.setZoomFactor(Config.WEB_ZOOM_FACTOR)
|
||||
self.last_update[widget] = datetime.now()
|
||||
self.last_update[widget] = dt.datetime.now()
|
||||
_ = self.addTab(widget, "")
|
||||
|
||||
def open_in_songfacts(self, title):
|
||||
@ -80,7 +80,7 @@ class InfoTabs(QTabWidget):
|
||||
self.setTabText(tab_index, short_title)
|
||||
|
||||
widget.setUrl(QUrl(url))
|
||||
self.last_update[widget] = datetime.now()
|
||||
self.last_update[widget] = dt.datetime.now()
|
||||
self.tabtitles[tab_index] = url
|
||||
|
||||
# Show newly updated tab
|
||||
|
||||
340
app/models.py
340
app/models.py
@ -1,68 +1,46 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import re
|
||||
|
||||
from config import Config
|
||||
from dbconfig import scoped_session
|
||||
|
||||
from datetime import datetime
|
||||
from pprint import pprint
|
||||
# Standard library imports
|
||||
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 (
|
||||
bindparam,
|
||||
Boolean,
|
||||
DateTime,
|
||||
delete,
|
||||
ForeignKey,
|
||||
func,
|
||||
select,
|
||||
String,
|
||||
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 (
|
||||
DeclarativeBase,
|
||||
joinedload,
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
from sqlalchemy.orm.exc import (
|
||||
NoResultFound,
|
||||
)
|
||||
from sqlalchemy.exc import (
|
||||
IntegrityError,
|
||||
)
|
||||
# App imports
|
||||
import dbtables
|
||||
from config import Config
|
||||
from log import log
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
# Establish database connection
|
||||
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
|
||||
class Carts(Base):
|
||||
__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 Carts(dbtables.CartsTable):
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
session: Session,
|
||||
cart_number: int,
|
||||
name: str,
|
||||
duration: Optional[int] = None,
|
||||
@ -81,26 +59,10 @@ class Carts(Base):
|
||||
session.commit()
|
||||
|
||||
|
||||
class NoteColours(Base):
|
||||
__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}>"
|
||||
)
|
||||
|
||||
class NoteColours(dbtables.NoteColoursTable):
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
session: Session,
|
||||
substring: str,
|
||||
colour: str,
|
||||
enabled: bool = True,
|
||||
@ -116,10 +78,10 @@ class NoteColours(Base):
|
||||
self.order = order
|
||||
|
||||
session.add(self)
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, session: scoped_session) -> Sequence["NoteColours"]:
|
||||
def get_all(cls, session: Session) -> Sequence["NoteColours"]:
|
||||
"""
|
||||
Return all records
|
||||
"""
|
||||
@ -127,7 +89,7 @@ class NoteColours(Base):
|
||||
return session.scalars(select(cls)).all()
|
||||
|
||||
@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
|
||||
"""
|
||||
@ -158,30 +120,17 @@ class NoteColours(Base):
|
||||
return None
|
||||
|
||||
|
||||
class Playdates(Base):
|
||||
__tablename__ = "playdates"
|
||||
|
||||
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:
|
||||
class Playdates(dbtables.PlaydatesTable):
|
||||
def __init__(self, session: Session, track_id: int) -> None:
|
||||
"""Record that track was played"""
|
||||
|
||||
self.lastplayed = datetime.now()
|
||||
self.lastplayed = dt.datetime.now()
|
||||
self.track_id = track_id
|
||||
session.add(self)
|
||||
session.commit()
|
||||
|
||||
@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"""
|
||||
|
||||
last_played = session.execute(
|
||||
@ -194,10 +143,12 @@ class Playdates(Base):
|
||||
if last_played:
|
||||
return last_played[0]
|
||||
else:
|
||||
return Config.EPOCH
|
||||
# Should never be reached as we create record with a
|
||||
# last_played value
|
||||
return Config.EPOCH # pragma: no cover
|
||||
|
||||
@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 session.scalars(
|
||||
@ -207,50 +158,20 @@ class Playdates(Base):
|
||||
).all()
|
||||
|
||||
|
||||
class Playlists(Base):
|
||||
"""
|
||||
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):
|
||||
class Playlists(dbtables.PlaylistsTable):
|
||||
def __init__(self, session: Session, name: str):
|
||||
self.name = name
|
||||
session.add(self)
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
@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
|
||||
"""
|
||||
|
||||
session.execute(
|
||||
update(Playlists)
|
||||
.where(
|
||||
(Playlists.id.in_(playlist_ids))
|
||||
)
|
||||
.values(tab=None)
|
||||
update(Playlists).where((Playlists.id.in_(playlist_ids))).values(tab=None)
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
@ -260,7 +181,7 @@ class Playlists(Base):
|
||||
|
||||
@classmethod
|
||||
def create_playlist_from_template(
|
||||
cls, session: scoped_session, template: "Playlists", playlist_name: str
|
||||
cls, session: Session, template: "Playlists", playlist_name: str
|
||||
) -> Optional["Playlists"]:
|
||||
"""Create a new playlist from template"""
|
||||
|
||||
@ -274,16 +195,16 @@ class Playlists(Base):
|
||||
|
||||
return playlist
|
||||
|
||||
def delete(self, session: scoped_session) -> None:
|
||||
def delete(self, session: Session) -> None:
|
||||
"""
|
||||
Mark as deleted
|
||||
"""
|
||||
|
||||
self.deleted = True
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
@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"""
|
||||
|
||||
return session.scalars(
|
||||
@ -293,7 +214,7 @@ class Playlists(Base):
|
||||
).all()
|
||||
|
||||
@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"""
|
||||
|
||||
return session.scalars(
|
||||
@ -301,7 +222,7 @@ class Playlists(Base):
|
||||
).all()
|
||||
|
||||
@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"""
|
||||
|
||||
return session.scalars(
|
||||
@ -315,7 +236,7 @@ class Playlists(Base):
|
||||
).all()
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -328,10 +249,10 @@ class Playlists(Base):
|
||||
"""Mark playlist as loaded and used now"""
|
||||
|
||||
self.open = True
|
||||
self.last_used = datetime.now()
|
||||
self.last_used = dt.datetime.now()
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -341,17 +262,17 @@ class Playlists(Base):
|
||||
is None
|
||||
)
|
||||
|
||||
def rename(self, session: scoped_session, new_name: str) -> None:
|
||||
def rename(self, session: Session, new_name: str) -> None:
|
||||
"""
|
||||
Rename playlist
|
||||
"""
|
||||
|
||||
self.name = new_name
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def save_as_template(
|
||||
session: scoped_session, playlist_id: int, template_name: str
|
||||
session: Session, playlist_id: int, template_name: str
|
||||
) -> None:
|
||||
"""Save passed playlist as new template"""
|
||||
|
||||
@ -365,35 +286,10 @@ class Playlists(Base):
|
||||
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
||||
|
||||
|
||||
class PlaylistRows(Base):
|
||||
__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}>"
|
||||
)
|
||||
|
||||
class PlaylistRows(dbtables.PlaylistRowsTable):
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
session: Session,
|
||||
playlist_id: int,
|
||||
row_number: int,
|
||||
note: str = "",
|
||||
@ -406,7 +302,7 @@ class PlaylistRows(Base):
|
||||
self.plr_rownum = row_number
|
||||
self.note = note
|
||||
session.add(self)
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
def append_note(self, extra_note: str) -> None:
|
||||
"""Append passed note to any existing note"""
|
||||
@ -418,7 +314,7 @@ class PlaylistRows(Base):
|
||||
self.note = extra_note
|
||||
|
||||
@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"""
|
||||
|
||||
src_rows = session.scalars(
|
||||
@ -436,7 +332,7 @@ class PlaylistRows(Base):
|
||||
|
||||
@classmethod
|
||||
def deep_row(
|
||||
cls, session: scoped_session, playlist_id: int, row_number: int
|
||||
cls, session: Session, playlist_id: int, row_number: int
|
||||
) -> "PlaylistRows":
|
||||
"""
|
||||
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()
|
||||
|
||||
@classmethod
|
||||
def deep_rows(
|
||||
cls, session: scoped_session, playlist_id: int
|
||||
) -> Sequence["PlaylistRows"]:
|
||||
def deep_rows(cls, session: Session, playlist_id: int) -> Sequence["PlaylistRows"]:
|
||||
"""
|
||||
Return a list of playlist rows that include full track and lastplayed data for
|
||||
given playlist_id., Sequence
|
||||
@ -475,9 +369,7 @@ class PlaylistRows(Base):
|
||||
return session.scalars(stmt).unique().all()
|
||||
|
||||
@staticmethod
|
||||
def delete_higher_rows(
|
||||
session: scoped_session, playlist_id: int, maxrow: int
|
||||
) -> None:
|
||||
def delete_higher_rows(session: Session, playlist_id: int, maxrow: int) -> None:
|
||||
"""
|
||||
Delete rows in given playlist that have a higher row number
|
||||
than 'maxrow'
|
||||
@ -489,10 +381,10 @@ class PlaylistRows(Base):
|
||||
PlaylistRows.plr_rownum > maxrow,
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -505,7 +397,7 @@ class PlaylistRows(Base):
|
||||
)
|
||||
|
||||
@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
|
||||
"""
|
||||
@ -524,7 +416,7 @@ class PlaylistRows(Base):
|
||||
|
||||
@classmethod
|
||||
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"]:
|
||||
"""
|
||||
Take a list of PlaylistRows ids and return a list of corresponding
|
||||
@ -540,7 +432,7 @@ class PlaylistRows(Base):
|
||||
return plrs
|
||||
|
||||
@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 session.execute(
|
||||
@ -551,7 +443,7 @@ class PlaylistRows(Base):
|
||||
|
||||
@staticmethod
|
||||
def get_track_plr(
|
||||
session: scoped_session, track_id: int, playlist_id: int
|
||||
session: Session, track_id: int, playlist_id: int
|
||||
) -> Optional["PlaylistRows"]:
|
||||
"""Return first matching PlaylistRows object or None"""
|
||||
|
||||
@ -566,7 +458,7 @@ class PlaylistRows(Base):
|
||||
|
||||
@classmethod
|
||||
def get_played_rows(
|
||||
cls, session: scoped_session, playlist_id: int
|
||||
cls, session: Session, playlist_id: int
|
||||
) -> Sequence["PlaylistRows"]:
|
||||
"""
|
||||
For passed playlist, return a list of rows that
|
||||
@ -584,10 +476,8 @@ class PlaylistRows(Base):
|
||||
@classmethod
|
||||
def get_rows_with_tracks(
|
||||
cls,
|
||||
session: scoped_session,
|
||||
session: Session,
|
||||
playlist_id: int,
|
||||
from_row: Optional[int] = None,
|
||||
to_row: Optional[int] = None,
|
||||
) -> Sequence["PlaylistRows"]:
|
||||
"""
|
||||
For passed playlist, return a list of rows that
|
||||
@ -597,18 +487,13 @@ class PlaylistRows(Base):
|
||||
query = select(cls).where(
|
||||
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()
|
||||
|
||||
return plrs
|
||||
|
||||
@classmethod
|
||||
def get_unplayed_rows(
|
||||
cls, session: scoped_session, playlist_id: int
|
||||
cls, session: Session, playlist_id: int
|
||||
) -> Sequence["PlaylistRows"]:
|
||||
"""
|
||||
For passed playlist, return a list of playlist rows that
|
||||
@ -629,14 +514,25 @@ class PlaylistRows(Base):
|
||||
|
||||
@classmethod
|
||||
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":
|
||||
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
|
||||
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:
|
||||
"""
|
||||
Create space to insert move_by additional rows by incremented row
|
||||
@ -656,7 +552,7 @@ class PlaylistRows(Base):
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
class Settings(Base):
|
||||
"""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[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):
|
||||
class Settings(dbtables.SettingsTable):
|
||||
def __init__(self, session: Session, name: str):
|
||||
self.name = name
|
||||
session.add(self)
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def all_as_dict(cls, session):
|
||||
@ -712,7 +592,7 @@ class Settings(Base):
|
||||
return result
|
||||
|
||||
@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"""
|
||||
|
||||
try:
|
||||
@ -721,45 +601,17 @@ class Settings(Base):
|
||||
except NoResultFound:
|
||||
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():
|
||||
assert hasattr(self, key)
|
||||
setattr(self, key, value)
|
||||
session.flush()
|
||||
session.commit()
|
||||
|
||||
|
||||
class Tracks(Base):
|
||||
__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}>"
|
||||
)
|
||||
|
||||
class Tracks(dbtables.TracksTable):
|
||||
def __init__(
|
||||
self,
|
||||
session: scoped_session,
|
||||
session: Session,
|
||||
path: str,
|
||||
title: str,
|
||||
artist: str,
|
||||
@ -795,7 +647,7 @@ class Tracks(Base):
|
||||
return session.scalars(select(cls)).unique().all()
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -810,7 +662,7 @@ class Tracks(Base):
|
||||
return None
|
||||
|
||||
@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
|
||||
|
||||
@ -831,7 +683,7 @@ class Tracks(Base):
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@ -1,22 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
from typing import (
|
||||
cast,
|
||||
List,
|
||||
Optional,
|
||||
)
|
||||
# Standard library imports
|
||||
from os.path import basename
|
||||
|
||||
from time import sleep
|
||||
from typing import cast, List, Optional
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import pipeclient
|
||||
from pygame import mixer
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import (
|
||||
pyqtSignal,
|
||||
QDate,
|
||||
@ -49,8 +44,14 @@ from PyQt6.QtWidgets import (
|
||||
QProgressBar,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
# Third party imports
|
||||
from pygame import mixer
|
||||
import pipeclient
|
||||
from sqlalchemy.orm.session import Session
|
||||
import stackprinter # type: ignore
|
||||
|
||||
# App imports
|
||||
from classes import (
|
||||
track_sequence,
|
||||
FadeCurve,
|
||||
@ -58,23 +59,18 @@ from classes import (
|
||||
PlaylistTrack,
|
||||
)
|
||||
from config import Config
|
||||
from dbconfig import (
|
||||
engine,
|
||||
scoped_session,
|
||||
Session,
|
||||
)
|
||||
from dialogs import TrackSelectDialog
|
||||
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 playlists import PlaylistTab
|
||||
from ui import icons_rc # noqa F401
|
||||
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
|
||||
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
||||
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
||||
from ui.main_window_ui import Ui_MainWindow # type: ignore
|
||||
from utilities import check_db, update_bitrates
|
||||
import helpers
|
||||
from ui import icons_rc # noqa F401
|
||||
import music
|
||||
|
||||
|
||||
@ -168,7 +164,7 @@ class ImportTrack(QObject):
|
||||
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:
|
||||
self.signals.status_message_signal.emit(
|
||||
f"Importing {basename(fname)}", 5000
|
||||
@ -208,14 +204,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.music: music.Music = music.Music()
|
||||
self.playing: bool = False
|
||||
|
||||
self.selected_plrs: Optional[List[PlaylistRows]] = None
|
||||
|
||||
self.set_main_window_size()
|
||||
self.lblSumPlaytime = QLabel("")
|
||||
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
|
||||
self.txtSearch = QLineEdit()
|
||||
self.statusbar.addWidget(self.txtSearch)
|
||||
self.txtSearch.setHidden(True)
|
||||
self.statusbar.addWidget(self.txtSearch)
|
||||
self.hide_played_tracks = False
|
||||
mixer.init()
|
||||
self.widgetFadeVolume.hideAxis("bottom")
|
||||
@ -257,7 +251,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
except subprocess.CalledProcessError as exc_info:
|
||||
git_tag = str(exc_info.output)
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
if session.bind:
|
||||
dbname = session.bind.engine.url.database
|
||||
|
||||
@ -319,7 +313,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def cart_edit(self, btn: CartButton, event: QEvent):
|
||||
"""Handle context menu for cart button"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
cart = session.query(Carts).get(btn.cart_id)
|
||||
if cart is None:
|
||||
log.error("cart_edit: cart not found")
|
||||
@ -351,7 +345,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def carts_init(self) -> None:
|
||||
"""Initialse carts data structures"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
# Number carts from 1 for humanity
|
||||
for cart_number in range(1, Config.CARTS_COUNT + 1):
|
||||
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"
|
||||
)
|
||||
else:
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
settings = Settings.all_as_dict(session)
|
||||
record = settings["mainwindow_height"]
|
||||
if record.f_int != self.height():
|
||||
@ -497,7 +491,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
return False
|
||||
|
||||
# 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)
|
||||
if playlist:
|
||||
playlist.close()
|
||||
@ -568,7 +562,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.timer1000.timeout.connect(self.tick_1000ms)
|
||||
|
||||
def create_playlist(
|
||||
self, session: scoped_session, playlist_name: Optional[str] = None
|
||||
self, session: Session, playlist_name: Optional[str] = None
|
||||
) -> Optional[Playlists]:
|
||||
"""Create new playlist"""
|
||||
|
||||
@ -590,7 +584,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def create_and_show_playlist(self) -> None:
|
||||
"""Create new playlist and display it"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
playlist = self.create_playlist(session)
|
||||
if playlist:
|
||||
self.create_playlist_tab(playlist)
|
||||
@ -638,7 +632,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
Delete current playlist
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
playlist_id = self.active_tab().playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
@ -672,7 +666,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
path += ".csv"
|
||||
|
||||
with open(path, "w") as f:
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
for playdate in Playdates.played_after(session, start_dt):
|
||||
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
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
# Get output filename
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
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:
|
||||
return 0
|
||||
|
||||
now = datetime.now()
|
||||
now = dt.datetime.now()
|
||||
track_start = track_sequence.now.start_time
|
||||
elapsed_seconds = (now - track_start).total_seconds()
|
||||
return int(elapsed_seconds * 1000)
|
||||
@ -786,7 +780,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if not dlg.exec():
|
||||
return
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
new_tracks = []
|
||||
for fname in dlg.selectedFiles():
|
||||
txt = ""
|
||||
@ -885,7 +879,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.active_tab().source_model_selected_row_number()
|
||||
or self.active_proxy_model().rowCount()
|
||||
)
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
dlg = TrackSelectDialog(
|
||||
session=session,
|
||||
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"""
|
||||
|
||||
playlist_ids = []
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
for playlist in Playlists.get_open(session):
|
||||
if playlist:
|
||||
_ = self.create_playlist_tab(playlist)
|
||||
playlist_ids.append(playlist.id)
|
||||
log.info(f"load_last_playlists() loaded {playlist=}")
|
||||
log.debug(f"load_last_playlists() loaded {playlist=}")
|
||||
# Set active tab
|
||||
record = Settings.get_int_settings(session, "active_tab")
|
||||
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()
|
||||
source_playlist_id = visible_tab.playlist_id
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
for playlist in Playlists.get_all(session):
|
||||
if playlist.id == source_playlist_id:
|
||||
continue
|
||||
@ -1007,7 +1001,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def new_from_template(self) -> None:
|
||||
"""Create new playlist from template"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
templates = Playlists.get_all_templates(session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=templates, session=session)
|
||||
dlg.exec()
|
||||
@ -1033,7 +1027,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def open_playlist(self) -> None:
|
||||
"""Open existing playlist"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
playlists = Playlists.get_closed(session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
||||
dlg.exec()
|
||||
@ -1199,7 +1193,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
Rename current playlist
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
playlist_id = self.active_tab().playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
@ -1243,12 +1237,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
and 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:
|
||||
"""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)]
|
||||
|
||||
while True:
|
||||
@ -1310,21 +1304,39 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
def set_main_window_size(self) -> None:
|
||||
"""Set size of window from database"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
settings = Settings.all_as_dict(session)
|
||||
record = settings["mainwindow_x"]
|
||||
x = record.f_int or 1
|
||||
record = settings["mainwindow_y"]
|
||||
y = record.f_int or 1
|
||||
record = settings["mainwindow_width"]
|
||||
width = record.f_int or 1599
|
||||
record = settings["mainwindow_height"]
|
||||
height = record.f_int or 981
|
||||
if "mainwindow_x" in settings:
|
||||
record = settings["mainwindow_x"]
|
||||
x = record.f_int or 1
|
||||
else:
|
||||
x = 100
|
||||
if "mainwindow_y" in settings:
|
||||
record = settings["mainwindow_y"]
|
||||
y = record.f_int or 1
|
||||
else:
|
||||
y = 100
|
||||
if "mainwindow_width" in settings:
|
||||
record = settings["mainwindow_width"]
|
||||
width = record.f_int or 1599
|
||||
else:
|
||||
width = 100
|
||||
if "mainwindow_height" in settings:
|
||||
record = settings["mainwindow_height"]
|
||||
height = record.f_int or 981
|
||||
else:
|
||||
height = 100
|
||||
self.setGeometry(x, y, width, height)
|
||||
record = settings["splitter_top"]
|
||||
splitter_top = record.f_int or 256
|
||||
record = settings["splitter_bottom"]
|
||||
splitter_bottom = record.f_int or 256
|
||||
if "splitter_top" in settings:
|
||||
record = settings["splitter_top"]
|
||||
splitter_top = record.f_int or 256
|
||||
else:
|
||||
splitter_top = 100
|
||||
if "splitter_bottom" in settings:
|
||||
record = settings["splitter_bottom"]
|
||||
splitter_bottom = record.f_int or 256
|
||||
else:
|
||||
splitter_bottom = 100
|
||||
self.splitter.setSizes([splitter_top, splitter_bottom])
|
||||
return
|
||||
|
||||
@ -1397,7 +1409,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.tabPlaylist.currentWidget().scroll_to_top(display_row)
|
||||
|
||||
def solicit_playlist_name(
|
||||
self, session: scoped_session, default: str = ""
|
||||
self, session: Session, default: str = ""
|
||||
) -> Optional[str]:
|
||||
"""Get name of new playlist from user"""
|
||||
|
||||
@ -1498,13 +1510,19 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
# 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 (
|
||||
track_sequence.now.track_id
|
||||
and track_sequence.now.fade_graph
|
||||
and track_sequence.now.start_time
|
||||
):
|
||||
play_time = (
|
||||
datetime.now() - track_sequence.now.start_time
|
||||
dt.datetime.now() - track_sequence.now.start_time
|
||||
).total_seconds() * 1000
|
||||
track_sequence.now.fade_graph.tick(play_time)
|
||||
|
||||
@ -1513,7 +1531,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
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
|
||||
# self.cart_tick()
|
||||
|
||||
@ -1541,8 +1559,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
and track_sequence.now.start_time
|
||||
and (
|
||||
self.music.player.is_playing()
|
||||
or (datetime.now() - track_sequence.now.start_time)
|
||||
< timedelta(microseconds=Config.PLAY_SETTLE)
|
||||
or (dt.datetime.now() - track_sequence.now.start_time)
|
||||
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
|
||||
)
|
||||
):
|
||||
playtime = self.get_playtime()
|
||||
@ -1631,7 +1649,7 @@ class CartDialog(QDialog):
|
||||
"""Edit cart details"""
|
||||
|
||||
def __init__(
|
||||
self, musicmuster: Window, session: scoped_session, cart: Carts, *args, **kwargs
|
||||
self, musicmuster: Window, session: Session, cart: Carts, *args, **kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Manage carts
|
||||
@ -1756,18 +1774,15 @@ if __name__ == "__main__":
|
||||
|
||||
# Run as required
|
||||
if args.check_db:
|
||||
log.debug("Updating database")
|
||||
with Session() as session:
|
||||
log.debug("Checking database")
|
||||
with db.Session() as session:
|
||||
check_db(session)
|
||||
engine.dispose()
|
||||
elif args.update_bitrates:
|
||||
log.debug("Update bitrates")
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
update_bitrates(session)
|
||||
engine.dispose()
|
||||
else:
|
||||
try:
|
||||
Base.metadata.create_all(engine)
|
||||
app = QApplication(sys.argv)
|
||||
# PyQt6 defaults to a grey for labels
|
||||
palette = app.palette()
|
||||
@ -1785,7 +1800,6 @@ if __name__ == "__main__":
|
||||
win = Window()
|
||||
win.show()
|
||||
status = app.exec()
|
||||
engine.dispose()
|
||||
sys.exit(status)
|
||||
except Exception as exc:
|
||||
if os.environ["MM_ENV"] == "PRODUCTION":
|
||||
|
||||
@ -81,22 +81,22 @@ import argparse
|
||||
|
||||
|
||||
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
|
||||
if sys.platform == 'win32':
|
||||
WRITE_NAME: str = '\\\\.\\pipe\\ToSrvPipe'
|
||||
READ_NAME: str = '\\\\.\\pipe\\FromSrvPipe'
|
||||
EOL: str = '\r\n\0'
|
||||
if sys.platform == "win32":
|
||||
WRITE_NAME: str = "\\\\.\\pipe\\ToSrvPipe"
|
||||
READ_NAME: str = "\\\\.\\pipe\\FromSrvPipe"
|
||||
EOL: str = "\r\n\0"
|
||||
else:
|
||||
# Linux or Mac
|
||||
PIPE_BASE: str = '/tmp/audacity_script_pipe.'
|
||||
WRITE_NAME: str = PIPE_BASE + 'to.' + str(os.getuid())
|
||||
READ_NAME: str = PIPE_BASE + 'from.' + str(os.getuid())
|
||||
EOL: str = '\n'
|
||||
PIPE_BASE: str = "/tmp/audacity_script_pipe."
|
||||
WRITE_NAME: str = PIPE_BASE + "to." + str(os.getuid())
|
||||
READ_NAME: str = PIPE_BASE + "from." + str(os.getuid())
|
||||
EOL: str = "\n"
|
||||
|
||||
|
||||
class PipeClient():
|
||||
class PipeClient:
|
||||
"""Write / read client access to Audacity via named pipes.
|
||||
|
||||
Normally there should be just one instance of this class. If
|
||||
@ -141,7 +141,7 @@ class PipeClient():
|
||||
self.timer: bool = False # type: ignore
|
||||
self._start_time: float = 0 # type: ignore
|
||||
self._write_pipe = None
|
||||
self.reply: str = '' # type: ignore
|
||||
self.reply: str = "" # type: ignore
|
||||
if not self._write_pipe:
|
||||
self._write_thread_start()
|
||||
self._read_thread_start()
|
||||
@ -156,11 +156,11 @@ class PipeClient():
|
||||
# Allow a little time for connection to be made.
|
||||
time.sleep(0.1)
|
||||
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:
|
||||
"""Open _write_pipe."""
|
||||
self._write_pipe = open(WRITE_NAME, 'w')
|
||||
self._write_pipe = open(WRITE_NAME, "w")
|
||||
|
||||
def _read_thread_start(self) -> None:
|
||||
"""Start read_pipe thread."""
|
||||
@ -187,16 +187,16 @@ class PipeClient():
|
||||
self._write_pipe.write(command + EOL)
|
||||
# Check that read pipe is alive
|
||||
if PipeClient.reader_pipe_broken.is_set():
|
||||
raise RuntimeError('PipeClient: Read-pipe error.')
|
||||
raise RuntimeError("PipeClient: Read-pipe error.")
|
||||
try:
|
||||
self._write_pipe.flush()
|
||||
if self.timer:
|
||||
self._start_time = time.time()
|
||||
self.reply = ''
|
||||
self.reply = ""
|
||||
PipeClient.reply_ready.clear()
|
||||
except IOError as err:
|
||||
if err.errno == errno.EPIPE:
|
||||
raise RuntimeError('PipeClient: Write-pipe error.')
|
||||
raise RuntimeError("PipeClient: Write-pipe error.")
|
||||
else:
|
||||
raise
|
||||
|
||||
@ -204,27 +204,27 @@ class PipeClient():
|
||||
"""Read FIFO in worker thread."""
|
||||
# Thread will wait at this read until it connects.
|
||||
# Connection should occur as soon as _write_pipe has connected.
|
||||
with open(READ_NAME, 'r') as read_pipe:
|
||||
message = ''
|
||||
with open(READ_NAME, "r") as read_pipe:
|
||||
message = ""
|
||||
pipe_ok = True
|
||||
while pipe_ok:
|
||||
line = read_pipe.readline()
|
||||
# Stop timer as soon as we get first line of response.
|
||||
stop_time = time.time()
|
||||
while pipe_ok and line != '\n':
|
||||
while pipe_ok and line != "\n":
|
||||
message += line
|
||||
line = read_pipe.readline()
|
||||
if line == '':
|
||||
if line == "":
|
||||
# No data in read_pipe indicates that the pipe
|
||||
# is broken (Audacity may have crashed).
|
||||
PipeClient.reader_pipe_broken.set()
|
||||
pipe_ok = False
|
||||
if self.timer:
|
||||
xtime = (stop_time - self._start_time) * 1000
|
||||
message += f'Execution time: {xtime:.2f}ms'
|
||||
message += f"Execution time: {xtime:.2f}ms"
|
||||
self.reply = message
|
||||
PipeClient.reply_ready.set()
|
||||
message = ''
|
||||
message = ""
|
||||
|
||||
def read(self) -> str:
|
||||
"""Read Audacity's reply from pipe.
|
||||
@ -238,31 +238,45 @@ class PipeClient():
|
||||
|
||||
"""
|
||||
if not PipeClient.reply_ready.is_set():
|
||||
return ''
|
||||
return ""
|
||||
return self.reply
|
||||
|
||||
|
||||
def bool_from_string(strval) -> bool:
|
||||
"""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
|
||||
if strval.lower() in ('false', 'f', '0', 'no', 'n'):
|
||||
if strval.lower() in ("false", "f", "0", "no", "n"):
|
||||
return False
|
||||
raise argparse.ArgumentTypeError('Boolean value expected.')
|
||||
raise argparse.ArgumentTypeError("Boolean value expected.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Interactive command-line for PipeClient"""
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-t', '--timeout', type=float, metavar='', default=10,
|
||||
help="timeout for reply in seconds (default: 10")
|
||||
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')
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--timeout",
|
||||
type=float,
|
||||
metavar="",
|
||||
default=10,
|
||||
help="timeout for reply in seconds (default: 10",
|
||||
)
|
||||
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()
|
||||
|
||||
if args.docs:
|
||||
@ -271,23 +285,23 @@ def main() -> None:
|
||||
|
||||
client: PipeClient = PipeClient()
|
||||
while True:
|
||||
reply: str = ''
|
||||
reply: str = ""
|
||||
message: str = input("\nEnter command or 'Q' to quit: ")
|
||||
start = time.time()
|
||||
if message.upper() == 'Q':
|
||||
if message.upper() == "Q":
|
||||
sys.exit(0)
|
||||
elif message == '':
|
||||
elif message == "":
|
||||
pass
|
||||
else:
|
||||
client.write(message, timer=args.show)
|
||||
while reply == '':
|
||||
while reply == "":
|
||||
time.sleep(0.1) # allow time for reply
|
||||
if time.time() - start > args.timeout:
|
||||
reply = 'PipeClient: Reply timed-out.'
|
||||
reply = "PipeClient: Reply timed-out."
|
||||
else:
|
||||
reply = client.read()
|
||||
print(reply)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
# Standard library imports
|
||||
# Allow forward reference to PlaylistModel
|
||||
from __future__ import annotations
|
||||
import obsws_python as obs # type: ignore
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from enum import auto, Enum
|
||||
from operator import attrgetter
|
||||
from random import shuffle
|
||||
from typing import List, Optional
|
||||
import datetime as dt
|
||||
import re
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import (
|
||||
QAbstractTableModel,
|
||||
QModelIndex,
|
||||
@ -23,9 +25,14 @@ from PyQt6.QtGui import (
|
||||
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 config import Config
|
||||
from dbconfig import scoped_session, Session
|
||||
from helpers import (
|
||||
file_is_unreadable,
|
||||
get_embedded_time,
|
||||
@ -34,7 +41,7 @@ from helpers import (
|
||||
set_track_metadata,
|
||||
)
|
||||
from log import log
|
||||
from models import NoteColours, Playdates, PlaylistRows, Tracks
|
||||
from models import db, NoteColours, Playdates, PlaylistRows, Tracks
|
||||
|
||||
|
||||
HEADER_NOTES_COLUMN = 1
|
||||
@ -62,13 +69,13 @@ class PlaylistRowData:
|
||||
self.artist: str = ""
|
||||
self.bitrate = 0
|
||||
self.duration: int = 0
|
||||
self.lastplayed: datetime = Config.EPOCH
|
||||
self.lastplayed: dt.datetime = Config.EPOCH
|
||||
self.path = ""
|
||||
self.played = False
|
||||
self.start_gap: Optional[int] = None
|
||||
self.title: str = ""
|
||||
self.start_time: Optional[datetime] = None
|
||||
self.end_time: Optional[datetime] = None
|
||||
self.start_time: Optional[dt.datetime] = None
|
||||
self.end_time: Optional[dt.datetime] = None
|
||||
|
||||
self.plrid: int = plr.id
|
||||
self.plr_rownum: int = plr.plr_rownum
|
||||
@ -116,7 +123,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
log.info(f"PlaylistModel.__init__({playlist_id=})")
|
||||
log.debug(f"PlaylistModel.__init__({playlist_id=})")
|
||||
|
||||
self.playlist_id = playlist_id
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -129,7 +136,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
||||
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
|
||||
PlaylistRows.fixup_rownumbers(session, playlist_id)
|
||||
# Populate self.playlist_rows
|
||||
@ -148,7 +155,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
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
|
||||
try:
|
||||
@ -165,7 +172,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
"Header row already has track associated"
|
||||
)
|
||||
return
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
plr = session.get(PlaylistRows, prd.plrid)
|
||||
if plr:
|
||||
# Add track to PlaylistRows
|
||||
@ -187,7 +194,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Header row
|
||||
if self.is_header_row(row):
|
||||
# Check for specific header colouring
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
note_colour = NoteColours.get_colour(session, prd.note)
|
||||
if note_colour:
|
||||
return QBrush(QColor(note_colour))
|
||||
@ -216,7 +223,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
|
||||
if column == Col.NOTE.value:
|
||||
if prd.note:
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
note_colour = NoteColours.get_colour(session, prd.note)
|
||||
if note_colour:
|
||||
return QBrush(QColor(note_colour))
|
||||
@ -275,7 +282,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
log.debug("Call OBS scene change")
|
||||
self.obs_scene_change(row_number)
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
# Update Playdates in database
|
||||
log.debug("update playdates")
|
||||
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.
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
for row_number in sorted(row_numbers, reverse=True):
|
||||
log.info(f"delete_rows(), {row_number=}")
|
||||
super().beginRemoveRows(QModelIndex(), row_number, row_number)
|
||||
@ -454,7 +461,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if playlist_id != self.playlist_id:
|
||||
log.debug(f"end_reset_model: not us ({self.playlist_id=})")
|
||||
return
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
self.refresh_data(session)
|
||||
super().endResetModel()
|
||||
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.
|
||||
"""
|
||||
|
||||
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):
|
||||
# We are adding to the end of the list
|
||||
@ -552,7 +559,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
else:
|
||||
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
|
||||
|
||||
def get_row_info(self, row_number: int) -> PlaylistRowData:
|
||||
@ -688,8 +695,9 @@ class PlaylistModel(QAbstractTableModel):
|
||||
< prd.plr_rownum
|
||||
)
|
||||
):
|
||||
section_end_time = track_sequence.now.end_time + timedelta(
|
||||
milliseconds=duration
|
||||
section_end_time = (
|
||||
track_sequence.now.end_time
|
||||
+ dt.timedelta(milliseconds=duration)
|
||||
)
|
||||
end_time_str = (
|
||||
", section end time "
|
||||
@ -744,23 +752,25 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self,
|
||||
proposed_row_number: Optional[int],
|
||||
track_id: Optional[int] = None,
|
||||
note: Optional[str] = None,
|
||||
note: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
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)
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
super().beginInsertRows(QModelIndex(), new_row_number, new_row_number)
|
||||
plr = PlaylistRows.insert_row(session, self.playlist_id, new_row_number)
|
||||
|
||||
plr.track_id = track_id
|
||||
if note:
|
||||
plr.note = note
|
||||
_ = PlaylistRows.insert_row(
|
||||
session=session,
|
||||
playlist_id=self.playlist_id,
|
||||
new_row_number=new_row_number,
|
||||
note=note,
|
||||
track_id=track_id,
|
||||
)
|
||||
|
||||
self.refresh_data(session)
|
||||
super().endInsertRows()
|
||||
@ -820,7 +830,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Mark row as unplayed
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
for row_number in row_numbers:
|
||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||
if not plr:
|
||||
@ -835,7 +845,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
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
|
||||
row_map: dict[int, int] = {}
|
||||
@ -883,7 +893,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
plrid = self.playlist_rows[oldrow].plrid
|
||||
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)
|
||||
# Update playlist_rows
|
||||
self.refresh_data(session)
|
||||
@ -899,7 +909,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
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=}"
|
||||
)
|
||||
|
||||
@ -912,7 +922,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Prepare destination playlist for a reset
|
||||
self.signals.begin_reset_model_signal.emit(to_playlist_id)
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
# Make room in destination playlist
|
||||
max_destination_row_number = PlaylistRows.get_last_used_row(
|
||||
session, to_playlist_id
|
||||
@ -962,7 +972,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
log.info(f"move_track_add_note({new_row_number=}, {existing_prd=}, {note=}")
|
||||
|
||||
if note:
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
plr = session.get(PlaylistRows, existing_prd.plrid)
|
||||
if plr:
|
||||
if plr.note:
|
||||
@ -1050,7 +1060,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Update display
|
||||
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 self.playlist_rows with playlist data
|
||||
@ -1071,7 +1081,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
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)
|
||||
if plr:
|
||||
plr.track_id = None
|
||||
@ -1085,7 +1095,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
track_id = self.playlist_rows[row_number].track_id
|
||||
if track_id:
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
track = session.get(Tracks, track_id)
|
||||
set_track_metadata(track)
|
||||
self.refresh_row(session, row_number)
|
||||
@ -1097,11 +1107,11 @@ class PlaylistModel(QAbstractTableModel):
|
||||
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
|
||||
# update the row number
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
if track_sequence.next.plr_rownum:
|
||||
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
|
||||
if next_plr:
|
||||
@ -1129,7 +1139,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
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]] = []
|
||||
temp: List[int] = []
|
||||
@ -1145,7 +1155,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
result.append(temp)
|
||||
result.reverse()
|
||||
|
||||
log.info(f"_reversed_contiguous_row_groups() returned: {result=}")
|
||||
log.debug(f"_reversed_contiguous_row_groups() returned: {result=}")
|
||||
return result
|
||||
|
||||
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
||||
@ -1158,7 +1168,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
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
|
||||
if playlist_id != self.playlist_id:
|
||||
@ -1205,8 +1215,8 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.signals.next_track_changed_signal.emit()
|
||||
return
|
||||
|
||||
# Update playing_track
|
||||
with Session() as session:
|
||||
# Update track_sequence
|
||||
with db.Session() as session:
|
||||
track_sequence.next = PlaylistTrack()
|
||||
try:
|
||||
plrid = self.playlist_rows[row_number].plrid
|
||||
@ -1246,7 +1256,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
row_number = index.row()
|
||||
column = index.column()
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
|
||||
if not plr:
|
||||
print(
|
||||
@ -1342,12 +1352,15 @@ class PlaylistModel(QAbstractTableModel):
|
||||
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] = []
|
||||
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]
|
||||
|
||||
# Reset start_time if this is the current row
|
||||
@ -1365,7 +1378,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
and 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
|
||||
update_rows.append(row_number)
|
||||
continue
|
||||
@ -1410,7 +1423,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
update_rows.append(row_number)
|
||||
|
||||
# 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
|
||||
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.is_played_row(source_row):
|
||||
# Don't hide current or next track
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
if track_sequence.next.plr_id:
|
||||
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
|
||||
if (
|
||||
@ -1486,9 +1499,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
== self.source_model.playlist_id
|
||||
):
|
||||
if track_sequence.now.start_time:
|
||||
if datetime.now() > (
|
||||
if dt.datetime.now() > (
|
||||
track_sequence.now.start_time
|
||||
+ timedelta(
|
||||
+ dt.timedelta(
|
||||
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
|
||||
)
|
||||
):
|
||||
@ -1556,7 +1569,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
self,
|
||||
proposed_row_number: Optional[int],
|
||||
track_id: Optional[int] = None,
|
||||
note: Optional[str] = None,
|
||||
note: str = "",
|
||||
) -> None:
|
||||
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 time
|
||||
from pprint import pprint
|
||||
from typing import Callable, cast, List, Optional, TYPE_CHECKING
|
||||
|
||||
# PyQt imports
|
||||
from PyQt6.QtCore import (
|
||||
QEvent,
|
||||
QModelIndex,
|
||||
@ -30,10 +31,12 @@ from PyQt6.QtWidgets import (
|
||||
QStyleOption,
|
||||
)
|
||||
|
||||
from dbconfig import Session
|
||||
from dialogs import TrackSelectDialog
|
||||
# Third party imports
|
||||
|
||||
# App imports
|
||||
from classes import MusicMusterSignals, track_sequence
|
||||
from config import Config
|
||||
from dialogs import TrackSelectDialog
|
||||
from helpers import (
|
||||
ask_yes_no,
|
||||
ms_to_mmss,
|
||||
@ -41,11 +44,11 @@ from helpers import (
|
||||
show_warning,
|
||||
)
|
||||
from log import log
|
||||
from models import Settings
|
||||
from models import db, Settings
|
||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from musicmuster import Window
|
||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||
|
||||
|
||||
class EscapeDelegate(QStyledItemDelegate):
|
||||
@ -335,7 +338,7 @@ class PlaylistTab(QTableView):
|
||||
if model_row_number is None:
|
||||
return
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
dlg = TrackSelectDialog(
|
||||
session=session,
|
||||
new_row_number=model_row_number,
|
||||
@ -536,7 +539,7 @@ class PlaylistTab(QTableView):
|
||||
# Resize rows if necessary
|
||||
self.resizeRowsToContents()
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
attr_name = f"playlist_col_{column_number}_width"
|
||||
record = Settings.get_int_settings(session, attr_name)
|
||||
record.f_int = self.columnWidth(column_number)
|
||||
@ -830,7 +833,7 @@ class PlaylistTab(QTableView):
|
||||
return
|
||||
|
||||
# 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):
|
||||
attr_name = f"playlist_col_{column_number}_width"
|
||||
record = Settings.get_int_settings(session, attr_name)
|
||||
|
||||
@ -4,20 +4,25 @@
|
||||
# the current directory contains a "better" version of the file than the
|
||||
# parent (eg, bettet bitrate).
|
||||
|
||||
# Standard library imports
|
||||
import os
|
||||
import pydymenu # type: ignore
|
||||
import shutil
|
||||
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 (
|
||||
get_tags,
|
||||
set_track_metadata,
|
||||
)
|
||||
|
||||
from models import Tracks
|
||||
from dbconfig import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from typing import List
|
||||
from models import db, Tracks
|
||||
|
||||
# ###################### SETTINGS #########################
|
||||
process_name_and_tags_matches = True
|
||||
@ -42,7 +47,7 @@ def main():
|
||||
# We only want to run this against the production database because
|
||||
# we will affect files in the common pool of tracks used by all
|
||||
# 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: ")
|
||||
if response != "c":
|
||||
sys.exit(0)
|
||||
@ -51,7 +56,7 @@ def main():
|
||||
assert source_dir != parent_dir
|
||||
|
||||
# Scan parent directory
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
all_tracks = Tracks.get_all(session)
|
||||
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]
|
||||
@ -239,7 +244,7 @@ def process_track(src, dst, title, artist, bitrate):
|
||||
if not do_processing:
|
||||
return
|
||||
|
||||
with Session() as session:
|
||||
with db.Session() as session:
|
||||
track = Tracks.get_by_path(session, dst)
|
||||
if track:
|
||||
# Update path, but workaround MariaDB bug
|
||||
|
||||
@ -15,7 +15,11 @@ class Ui_MainWindow(object):
|
||||
MainWindow.resize(1280, 857)
|
||||
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
|
||||
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.setStyleSheet("")
|
||||
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
|
||||
@ -27,39 +31,62 @@ class Ui_MainWindow(object):
|
||||
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout_3.setObjectName("verticalLayout_3")
|
||||
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.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.setMaximumSize(QtCore.QSize(230, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("Sans")
|
||||
font.setPointSize(20)
|
||||
self.previous_track_2.setFont(font)
|
||||
self.previous_track_2.setStyleSheet("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.setStyleSheet(
|
||||
"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.setObjectName("previous_track_2")
|
||||
self.verticalLayout_3.addWidget(self.previous_track_2)
|
||||
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.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.setMaximumSize(QtCore.QSize(230, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setFamily("Sans")
|
||||
font.setPointSize(20)
|
||||
self.current_track_2.setFont(font)
|
||||
self.current_track_2.setStyleSheet("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.setStyleSheet(
|
||||
"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.setObjectName("current_track_2")
|
||||
self.verticalLayout_3.addWidget(self.current_track_2)
|
||||
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.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
|
||||
@ -69,19 +96,29 @@ class Ui_MainWindow(object):
|
||||
font.setFamily("Sans")
|
||||
font.setPointSize(20)
|
||||
self.next_track_2.setFont(font)
|
||||
self.next_track_2.setStyleSheet("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.setStyleSheet(
|
||||
"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.setObjectName("next_track_2")
|
||||
self.verticalLayout_3.addWidget(self.next_track_2)
|
||||
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout()
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
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.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth())
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
|
||||
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
|
||||
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
|
||||
@ -89,32 +126,43 @@ class Ui_MainWindow(object):
|
||||
font.setFamily("Sans")
|
||||
font.setPointSize(20)
|
||||
self.hdrPreviousTrack.setFont(font)
|
||||
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
|
||||
"border: 1px solid rgb(85, 87, 83);")
|
||||
self.hdrPreviousTrack.setStyleSheet(
|
||||
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
|
||||
)
|
||||
self.hdrPreviousTrack.setText("")
|
||||
self.hdrPreviousTrack.setWordWrap(False)
|
||||
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
|
||||
self.verticalLayout.addWidget(self.hdrPreviousTrack)
|
||||
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.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth())
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(20)
|
||||
self.hdrCurrentTrack.setFont(font)
|
||||
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
|
||||
"border: 1px solid rgb(85, 87, 83);\n"
|
||||
"text-align: left;\n"
|
||||
"padding-left: 8px;\n"
|
||||
"")
|
||||
self.hdrCurrentTrack.setStyleSheet(
|
||||
"background-color: #d4edda;\n"
|
||||
"border: 1px solid rgb(85, 87, 83);\n"
|
||||
"text-align: left;\n"
|
||||
"padding-left: 8px;\n"
|
||||
""
|
||||
)
|
||||
self.hdrCurrentTrack.setText("")
|
||||
self.hdrCurrentTrack.setFlat(True)
|
||||
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
|
||||
self.verticalLayout.addWidget(self.hdrCurrentTrack)
|
||||
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.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
|
||||
@ -122,10 +170,12 @@ class Ui_MainWindow(object):
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(20)
|
||||
self.hdrNextTrack.setFont(font)
|
||||
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
|
||||
"border: 1px solid rgb(85, 87, 83);\n"
|
||||
"text-align: left;\n"
|
||||
"padding-left: 8px;")
|
||||
self.hdrNextTrack.setStyleSheet(
|
||||
"background-color: #fff3cd;\n"
|
||||
"border: 1px solid rgb(85, 87, 83);\n"
|
||||
"text-align: left;\n"
|
||||
"padding-left: 8px;"
|
||||
)
|
||||
self.hdrNextTrack.setText("")
|
||||
self.hdrNextTrack.setFlat(True)
|
||||
self.hdrNextTrack.setObjectName("hdrNextTrack")
|
||||
@ -160,7 +210,12 @@ class Ui_MainWindow(object):
|
||||
self.cartsWidget.setObjectName("cartsWidget")
|
||||
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
|
||||
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.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
|
||||
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.setMinimumSize(QtCore.QSize(132, 41))
|
||||
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.setIconSize(QtCore.QSize(30, 30))
|
||||
self.btnPreview.setCheckable(True)
|
||||
@ -289,10 +348,15 @@ class Ui_MainWindow(object):
|
||||
self.label_silent_timer.setObjectName("label_silent_timer")
|
||||
self.horizontalLayout.addWidget(self.frame_silent)
|
||||
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.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.widgetFadeVolume.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
self.widgetFadeVolume.setSizePolicy(sizePolicy)
|
||||
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
|
||||
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
|
||||
@ -309,7 +373,11 @@ class Ui_MainWindow(object):
|
||||
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
|
||||
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
|
||||
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.setIconSize(QtCore.QSize(30, 30))
|
||||
self.btnFade.setObjectName("btnFade")
|
||||
@ -317,7 +385,11 @@ class Ui_MainWindow(object):
|
||||
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
|
||||
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
|
||||
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.setObjectName("btnStop")
|
||||
self.verticalLayout_5.addWidget(self.btnStop)
|
||||
@ -343,39 +415,69 @@ class Ui_MainWindow(object):
|
||||
MainWindow.setStatusBar(self.statusbar)
|
||||
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
|
||||
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.setObjectName("actionPlay_next")
|
||||
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
|
||||
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.setObjectName("actionSkipToNext")
|
||||
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
|
||||
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.setObjectName("actionInsertTrack")
|
||||
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
|
||||
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.setObjectName("actionAdd_file")
|
||||
self.actionFade = QtGui.QAction(parent=MainWindow)
|
||||
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.setObjectName("actionFade")
|
||||
self.actionStop = QtGui.QAction(parent=MainWindow)
|
||||
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.setObjectName("actionStop")
|
||||
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
|
||||
self.action_Clear_selection.setObjectName("action_Clear_selection")
|
||||
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
|
||||
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.setObjectName("action_Resume_previous")
|
||||
self.actionE_xit = QtGui.QAction(parent=MainWindow)
|
||||
@ -422,7 +524,9 @@ class Ui_MainWindow(object):
|
||||
self.actionImport = QtGui.QAction(parent=MainWindow)
|
||||
self.actionImport.setObjectName("actionImport")
|
||||
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.setObjectName("actionSearch")
|
||||
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
|
||||
@ -450,9 +554,13 @@ class Ui_MainWindow(object):
|
||||
self.actionResume = QtGui.QAction(parent=MainWindow)
|
||||
self.actionResume.setObjectName("actionResume")
|
||||
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.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.setObjectName("actionSelect_duplicate_rows")
|
||||
self.menuFile.addAction(self.actionNewPlaylist)
|
||||
@ -503,7 +611,7 @@ class Ui_MainWindow(object):
|
||||
self.retranslateUi(MainWindow)
|
||||
self.tabPlaylist.setCurrentIndex(-1)
|
||||
self.tabInfolist.setCurrentIndex(-1)
|
||||
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
|
||||
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
|
||||
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
@ -539,38 +647,58 @@ class Ui_MainWindow(object):
|
||||
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
|
||||
self.actionStop.setText(_translate("MainWindow", "S&top"))
|
||||
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_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.actionTest.setText(_translate("MainWindow", "&Test"))
|
||||
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
|
||||
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
|
||||
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.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
|
||||
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
|
||||
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.actionSetNext.setText(_translate("MainWindow", "Set &next"))
|
||||
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_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_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
|
||||
self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
|
||||
self.actionSelect_played_tracks.setText(
|
||||
_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.setShortcut(_translate("MainWindow", "Ctrl+T"))
|
||||
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
|
||||
self.actionImport.setText(_translate("MainWindow", "Import track..."))
|
||||
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.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.actionRemove.setText(_translate("MainWindow", "&Remove track"))
|
||||
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.setShortcut(_translate("MainWindow", "P"))
|
||||
self.action_About.setText(_translate("MainWindow", "&About"))
|
||||
self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
|
||||
self.actionNew_from_template.setText(_translate("MainWindow", "New from template..."))
|
||||
self.actionSave_as_template.setText(
|
||||
_translate("MainWindow", "Save as template...")
|
||||
)
|
||||
self.actionNew_from_template.setText(
|
||||
_translate("MainWindow", "New from template...")
|
||||
)
|
||||
self.actionDebug.setText(_translate("MainWindow", "Debug"))
|
||||
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
|
||||
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.actionResume.setText(_translate("MainWindow", "Resume"))
|
||||
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
|
||||
self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia"))
|
||||
self.actionSearch_title_in_Wikipedia.setShortcut(_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..."))
|
||||
self.actionSearch_title_in_Wikipedia.setText(
|
||||
_translate("MainWindow", "Search title in Wikipedia")
|
||||
)
|
||||
self.actionSearch_title_in_Wikipedia.setShortcut(
|
||||
_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 pyqtgraph import PlotWidget
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import datetime as dt
|
||||
from threading import Timer
|
||||
from pydub import AudioSegment
|
||||
from time import sleep
|
||||
from timeloop import Timeloop
|
||||
import vlc
|
||||
from timeloop import Timeloop # type: ignore
|
||||
import vlc # type: ignore
|
||||
|
||||
|
||||
class RepeatedTimer(object):
|
||||
@ -49,9 +49,9 @@ def leading_silence(audio_segment, silence_threshold=-50.0, chunk_size=10):
|
||||
|
||||
trim_ms = 0 # ms
|
||||
assert chunk_size > 0 # to avoid infinite loop
|
||||
while (
|
||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < silence_threshold
|
||||
and trim_ms < len(audio_segment)):
|
||||
while audio_segment[
|
||||
trim_ms : trim_ms + chunk_size
|
||||
].dBFS < silence_threshold and trim_ms < len(audio_segment):
|
||||
trim_ms += chunk_size
|
||||
|
||||
# if there is no end it should return the length of the segment
|
||||
@ -72,8 +72,9 @@ def significant_fade(audio_segment, fade_threshold=-20.0, chunk_size=10):
|
||||
segment_length = audio_segment.duration_seconds * 1000 # ms
|
||||
trim_ms = segment_length - chunk_size
|
||||
while (
|
||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold
|
||||
and trim_ms > 0):
|
||||
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
|
||||
and trim_ms > 0
|
||||
):
|
||||
trim_ms -= chunk_size
|
||||
|
||||
# if there is no trailing silence, return lenght of track (it's less
|
||||
@ -94,8 +95,9 @@ def trailing_silence(audio_segment, silence_threshold=-50.0, chunk_size=10):
|
||||
segment_length = audio_segment.duration_seconds * 1000 # ms
|
||||
trim_ms = segment_length - chunk_size
|
||||
while (
|
||||
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < silence_threshold
|
||||
and trim_ms > 0):
|
||||
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < silence_threshold
|
||||
and trim_ms > 0
|
||||
):
|
||||
trim_ms -= chunk_size
|
||||
|
||||
# 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
|
||||
talk_time = remaining_time - (total_time - talk_at)
|
||||
silent_time = remaining_time - (total_time - silent_at)
|
||||
end_time = (datetime.now() + timedelta(
|
||||
milliseconds=remaining_time)).strftime("%H:%M:%S")
|
||||
end_time = (dt.datetime.now() + timedelta(milliseconds=remaining_time)).strftime(
|
||||
"%H:%M:%S"
|
||||
)
|
||||
print(
|
||||
f"\t{ms_to_mmss(elapsed_time)}/"
|
||||
f"{ms_to_mmss(total_time)}\t\t"
|
||||
f"Talk in: {ms_to_mmss(talk_time)} "
|
||||
f"Silent in: {ms_to_mmss(silent_time)} "
|
||||
f"Ends at: {end_time} [{ms_to_mmss(remaining_time)}]"
|
||||
, end="\r")
|
||||
f"Ends at: {end_time} [{ms_to_mmss(remaining_time)}]",
|
||||
end="\r",
|
||||
)
|
||||
|
||||
|
||||
# Print name of current song, print name of next song. Play current when
|
||||
@ -163,21 +167,21 @@ def test():
|
||||
|
||||
test()
|
||||
# next_song = get_next_song
|
||||
#
|
||||
#
|
||||
# def play_track():
|
||||
# r = run_aud_cmd("--current-song-length
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
# def play():
|
||||
# play_track()
|
||||
# songtimer_start()
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
# print("Start playing in 3 seconds")
|
||||
#
|
||||
#
|
||||
# sleep(3)
|
||||
#
|
||||
#
|
||||
# play()
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
# tl = Timeloop()
|
||||
#
|
||||
#
|
||||
@ -48,34 +46,34 @@
|
||||
# rt.stop() # better in a try/finally block to make sure the program ends!
|
||||
# print("End")
|
||||
|
||||
#def kae2(self, index):
|
||||
# print(f"table header click, index={index}")
|
||||
# def kae2(self, index):
|
||||
# print(f"table header click, index={index}")
|
||||
|
||||
#def kae(self, a, b, c):
|
||||
# self.data.append(f"a={a}, b={b}, c={c}")
|
||||
# def kae(self, a, b, c):
|
||||
# self.data.append(f"a={a}, b={b}, c={c}")
|
||||
|
||||
#def mousePressEvent(self, QMouseEvent):
|
||||
# print("mouse press")
|
||||
# def mousePressEvent(self, QMouseEvent):
|
||||
# print("mouse press")
|
||||
|
||||
#def mouseReleaseEvent(self, QMouseEvent):
|
||||
# print("mouse release")
|
||||
# # QMessageBox.about(
|
||||
# # self,
|
||||
# # "About Sample Editor",
|
||||
# # "\n".join(self.data)
|
||||
# # )
|
||||
#def eventFilter(self, obj, event):
|
||||
# # you could be doing different groups of actions
|
||||
# # for different types of widgets and either filtering
|
||||
# # the event or not.
|
||||
# # Here we just check if its one of the layout widgets
|
||||
# # if self.layout.indexOf(obj) != -1:
|
||||
# # print(f"event received: {event.type()}")
|
||||
# if event.type() == QEvent.MouseButtonPress:
|
||||
# print("Widget click")
|
||||
# # if I returned True right here, the event
|
||||
# # would be filtered and not reach the obj,
|
||||
# # meaning that I decided to handle it myself
|
||||
# def mouseReleaseEvent(self, QMouseEvent):
|
||||
# print("mouse release")
|
||||
# # QMessageBox.about(
|
||||
# # self,
|
||||
# # "About Sample Editor",
|
||||
# # "\n".join(self.data)
|
||||
# # )
|
||||
# def eventFilter(self, obj, event):
|
||||
# # you could be doing different groups of actions
|
||||
# # for different types of widgets and either filtering
|
||||
# # the event or not.
|
||||
# # Here we just check if its one of the layout widgets
|
||||
# # if self.layout.indexOf(obj) != -1:
|
||||
# # print(f"event received: {event.type()}")
|
||||
# if event.type() == QEvent.MouseButtonPress:
|
||||
# print("Widget click")
|
||||
# # if I returned True right here, the event
|
||||
# # would be filtered and not reach the obj,
|
||||
# # meaning that I decided to handle it myself
|
||||
|
||||
# # regardless, just do the default
|
||||
# return super().eventFilter(obj, event)
|
||||
# # regardless, just do the default
|
||||
# return super().eventFilter(obj, event)
|
||||
|
||||
@ -7,19 +7,19 @@ from PyQt5.QtCore import Qt
|
||||
|
||||
qt_creator_file = "mainwindow.ui"
|
||||
Ui_MainWindow, QtBaseClass = uic.loadUiType(qt_creator_file)
|
||||
tick = QtGui.QImage('tick.png')
|
||||
tick = QtGui.QImage("tick.png")
|
||||
|
||||
|
||||
class TodoModel(QtCore.QAbstractListModel):
|
||||
def __init__(self, *args, todos=None, **kwargs):
|
||||
super(TodoModel, self).__init__(*args, **kwargs)
|
||||
self.todos = todos or []
|
||||
|
||||
|
||||
def data(self, index, role):
|
||||
if role == Qt.DisplayRole:
|
||||
_, text = self.todos[index.row()]
|
||||
return text
|
||||
|
||||
|
||||
if role == Qt.DecorationRole:
|
||||
status, _ = self.todos[index.row()]
|
||||
if status:
|
||||
@ -51,15 +51,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
and then clearing it.
|
||||
"""
|
||||
text = self.todoEdit.text()
|
||||
if text: # Don't add empty strings.
|
||||
if text: # Don't add empty strings.
|
||||
# Access the list via the model.
|
||||
self.model.todos.append((False, text))
|
||||
# Trigger refresh.
|
||||
# Trigger refresh.
|
||||
self.model.layoutChanged.emit()
|
||||
# Empty the input
|
||||
# Empty the input
|
||||
self.todoEdit.setText("")
|
||||
self.save()
|
||||
|
||||
|
||||
def delete(self):
|
||||
indexes = self.todoView.selectedIndexes()
|
||||
if indexes:
|
||||
@ -71,7 +71,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
# Clear the selection (as it is no longer valid).
|
||||
self.todoView.clearSelection()
|
||||
self.save()
|
||||
|
||||
|
||||
def complete(self):
|
||||
indexes = self.todoView.selectedIndexes()
|
||||
if indexes:
|
||||
@ -79,22 +79,22 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
row = index.row()
|
||||
status, text = self.model.todos[row]
|
||||
self.model.todos[row] = (True, text)
|
||||
# .dataChanged takes top-left and bottom right, which are equal
|
||||
# .dataChanged takes top-left and bottom right, which are equal
|
||||
# for a single selection.
|
||||
self.model.dataChanged.emit(index, index)
|
||||
# Clear the selection (as it is no longer valid).
|
||||
self.todoView.clearSelection()
|
||||
self.save()
|
||||
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
with open('data.db', 'r') as f:
|
||||
with open("data.db", "r") as f:
|
||||
self.model.todos = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
with open('data.db', 'w') as f:
|
||||
with open("data.db", "w") as f:
|
||||
data = json.dump(self.model.todos, f)
|
||||
|
||||
|
||||
@ -102,5 +102,3 @@ app = QtWidgets.QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
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
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0c604bf490f8'
|
||||
down_revision = '29c0d7ffc741'
|
||||
revision = "0c604bf490f8"
|
||||
down_revision = "29c0d7ffc741"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('playlist_rows', sa.Column('played', sa.Boolean(), nullable=False))
|
||||
op.drop_index('ix_tracks_lastplayed', table_name='tracks')
|
||||
op.drop_column('tracks', 'lastplayed')
|
||||
op.add_column("playlist_rows", sa.Column("played", sa.Boolean(), nullable=False))
|
||||
op.drop_index("ix_tracks_lastplayed", table_name="tracks")
|
||||
op.drop_column("tracks", "lastplayed")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('tracks', sa.Column('lastplayed', mysql.DATETIME(), nullable=True))
|
||||
op.create_index('ix_tracks_lastplayed', 'tracks', ['lastplayed'], unique=False)
|
||||
op.drop_column('playlist_rows', 'played')
|
||||
op.add_column("tracks", sa.Column("lastplayed", mysql.DATETIME(), nullable=True))
|
||||
op.create_index("ix_tracks_lastplayed", "tracks", ["lastplayed"], unique=False)
|
||||
op.drop_column("playlist_rows", "played")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
@ -10,21 +10,21 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2cc37d3cf07f'
|
||||
down_revision = 'e3b04db5506f'
|
||||
revision = "2cc37d3cf07f"
|
||||
down_revision = "e3b04db5506f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### 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('loaded', sa.Boolean(), 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))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('playlists', 'loaded')
|
||||
op.drop_column('playlists', 'last_used')
|
||||
op.drop_column("playlists", "loaded")
|
||||
op.drop_column("playlists", "last_used")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
@ -10,27 +10,28 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b0983648595e'
|
||||
down_revision = '1bc727e5e87f'
|
||||
revision = "b0983648595e"
|
||||
down_revision = "1bc727e5e87f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('settings',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('name', sa.String(length=32), nullable=False),
|
||||
sa.Column('f_datetime', sa.DateTime(), nullable=True),
|
||||
sa.Column('f_int', sa.Integer(), nullable=True),
|
||||
sa.Column('f_string', sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
op.create_table(
|
||||
"settings",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("name", sa.String(length=32), nullable=False),
|
||||
sa.Column("f_datetime", sa.DateTime(), nullable=True),
|
||||
sa.Column("f_int", sa.Integer(), nullable=True),
|
||||
sa.Column("f_string", sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("name"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('settings')
|
||||
op.drop_table("settings")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
@ -10,43 +10,54 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f07b96a5e60f'
|
||||
down_revision = 'b0983648595e'
|
||||
revision = "f07b96a5e60f"
|
||||
down_revision = "b0983648595e"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('playdates',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('lastplayed', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table(
|
||||
"playdates",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("lastplayed", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f('ix_playdates_lastplayed'), 'playdates', ['lastplayed'], unique=False)
|
||||
op.create_table('playlists',
|
||||
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_index(
|
||||
op.f("ix_playdates_lastplayed"), "playdates", ["lastplayed"], unique=False
|
||||
)
|
||||
op.create_table('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.create_table(
|
||||
"playlists",
|
||||
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.add_column('tracks', sa.Column('playdates_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'tracks', 'playdates', ['playdates_id'], ['id'])
|
||||
op.create_table(
|
||||
"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 ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'tracks', type_='foreignkey')
|
||||
op.drop_column('tracks', 'playdates_id')
|
||||
op.drop_table('playlistracks')
|
||||
op.drop_table('playlists')
|
||||
op.drop_index(op.f('ix_playdates_lastplayed'), table_name='playdates')
|
||||
op.drop_table('playdates')
|
||||
op.drop_constraint(None, "tracks", type_="foreignkey")
|
||||
op.drop_column("tracks", "playdates_id")
|
||||
op.drop_table("playlistracks")
|
||||
op.drop_table("playlists")
|
||||
op.drop_index(op.f("ix_playdates_lastplayed"), table_name="playdates")
|
||||
op.drop_table("playdates")
|
||||
# ### 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>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
tinytag = "^1.7.0"
|
||||
SQLAlchemy = "^2.0.22"
|
||||
python-vlc = "^3.0.12118"
|
||||
mysqlclient = "^2.1.0"
|
||||
mutagen = "^1.45.1"
|
||||
alembic = "^1.7.5"
|
||||
psutil = "^5.9.0"
|
||||
python = "^3.11"
|
||||
tinytag = "^1.10.1"
|
||||
SQLAlchemy = "^2.0.29"
|
||||
python-vlc = "^3.0.20123"
|
||||
mysqlclient = "^2.2.4"
|
||||
mutagen = "^1.47.0"
|
||||
alembic = "^1.13.1"
|
||||
psutil = "^5.9.8"
|
||||
pydub = "^0.25.1"
|
||||
types-psutil = "^5.8.22"
|
||||
python-slugify = "^6.1.2"
|
||||
types-psutil = "^5.9.5.20240316"
|
||||
python-slugify = "^8.0.4"
|
||||
thefuzz = "^0.19.0"
|
||||
python-Levenshtein = "^0.12.2"
|
||||
pyfzf = "^0.3.1"
|
||||
pydymenu = "^0.5.2"
|
||||
stackprinter = "^0.2.10"
|
||||
obsws-python = "^1.4.2"
|
||||
pyqt6 = "^6.5.0"
|
||||
pyqt6-webengine = "^6.5.0"
|
||||
pygame = "^2.4.0"
|
||||
obsws-python = "^1.7.0"
|
||||
pyqt6 = "^6.6.1"
|
||||
pyqt6-webengine = "^6.6.0"
|
||||
pygame = "^2.5.2"
|
||||
pyqtgraph = "^0.13.3"
|
||||
colorlog = "^6.8.0"
|
||||
colorlog = "^6.8.2"
|
||||
alchemical = "^1.0.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
ipdb = "^0.13.9"
|
||||
pytest = "^7.0.1"
|
||||
pytest-qt = "^4.0.2"
|
||||
pytest-qt = "^4.4.0"
|
||||
pydub-stubs = "^0.25.1"
|
||||
line-profiler = "^4.0.2"
|
||||
line-profiler = "^4.1.2"
|
||||
flakehell = "^0.9.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pudb = "^2023.1"
|
||||
pudb = "*"
|
||||
sphinx = "^7.0.1"
|
||||
furo = "^2023.5.20"
|
||||
black = "^24.2.0"
|
||||
flakehell = "^0.9.0"
|
||||
mypy = "^1.7.0"
|
||||
pdbp = "^1.5.0"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest = "^8.1.1"
|
||||
snoop = "^0.4.3"
|
||||
black = "^24.3.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
@ -56,7 +59,7 @@ explicit_package_bases = true
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--exitfirst --showlocals --capture=no"
|
||||
pythonpath = [".", "app"]
|
||||
filterwarnings = "ignore:'audioop' is deprecated"
|
||||
filterwarnings = ["ignore:'audioop' is deprecated", "ignore:pkg_resources"]
|
||||
|
||||
[tool.vulture]
|
||||
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