Compare commits
No commits in common. "805053b7953561edb9f6c9f29749357a46b97ddf" and "976eb91e30a2669364d3b9f796b78b65b11d9433" have entirely different histories.
805053b795
...
976eb91e30
@ -30,7 +30,7 @@ testing = False
|
|||||||
if MM_ENV == 'PRODUCTION':
|
if MM_ENV == 'PRODUCTION':
|
||||||
dbname = os.environ.get('MM_PRODUCTION_DBNAME', 'musicmuster_prod')
|
dbname = os.environ.get('MM_PRODUCTION_DBNAME', 'musicmuster_prod')
|
||||||
dbuser = os.environ.get('MM_PRODUCTION_DBUSER', 'musicmuster')
|
dbuser = os.environ.get('MM_PRODUCTION_DBUSER', 'musicmuster')
|
||||||
dbpw = os.environ.get('MM_PRODUCTION_DBPW', 'musicmuster')
|
dbpw = os.environ.get('MM_PRODUCTION_DBPW', 'xxxmusicmuster')
|
||||||
dbhost = os.environ.get('MM_PRODUCTION_DBHOST', 'localhost')
|
dbhost = os.environ.get('MM_PRODUCTION_DBHOST', 'localhost')
|
||||||
elif MM_ENV == 'TESTING':
|
elif MM_ENV == 'TESTING':
|
||||||
dbname = os.environ.get('MM_TESTING_DBNAME', 'musicmuster_testing')
|
dbname = os.environ.get('MM_TESTING_DBNAME', 'musicmuster_testing')
|
||||||
|
|||||||
111
app/models.py
111
app/models.py
@ -17,10 +17,9 @@ from sqlalchemy import (
|
|||||||
DateTime,
|
DateTime,
|
||||||
Float,
|
Float,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
func,
|
|
||||||
Integer,
|
Integer,
|
||||||
String,
|
String,
|
||||||
UniqueConstraint,
|
func
|
||||||
)
|
)
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
@ -40,6 +39,7 @@ from helpers import (
|
|||||||
)
|
)
|
||||||
from log import DEBUG, ERROR
|
from log import DEBUG, ERROR
|
||||||
|
|
||||||
|
|
||||||
Base: DeclarativeMeta = declarative_base()
|
Base: DeclarativeMeta = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
@ -97,9 +97,9 @@ class NoteColours(Base):
|
|||||||
|
|
||||||
for rec in (
|
for rec in (
|
||||||
session.query(NoteColours)
|
session.query(NoteColours)
|
||||||
.filter(NoteColours.enabled.is_(True))
|
.filter(NoteColours.enabled.is_(True))
|
||||||
.order_by(NoteColours.order)
|
.order_by(NoteColours.order)
|
||||||
.all()
|
.all()
|
||||||
):
|
):
|
||||||
if rec.is_regex:
|
if rec.is_regex:
|
||||||
flags = re.UNICODE
|
flags = re.UNICODE
|
||||||
@ -206,8 +206,8 @@ class Playdates(Base):
|
|||||||
|
|
||||||
last_played: Optional[Playdates] = session.query(
|
last_played: Optional[Playdates] = session.query(
|
||||||
Playdates.lastplayed).filter(
|
Playdates.lastplayed).filter(
|
||||||
(Playdates.track_id == track_id)
|
(Playdates.track_id == track_id)
|
||||||
).order_by(Playdates.lastplayed.desc()).first()
|
).order_by(Playdates.lastplayed.desc()).first()
|
||||||
if last_played:
|
if last_played:
|
||||||
return last_played[0]
|
return last_played[0]
|
||||||
else:
|
else:
|
||||||
@ -265,7 +265,7 @@ class Playlists(Base):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
row = PlaylistTracks.next_free_row(session, self.id)
|
row = PlaylistTracks.next_free_row(session, self)
|
||||||
|
|
||||||
PlaylistTracks(session, self.id, track_id, row)
|
PlaylistTracks(session, self.id, track_id, row)
|
||||||
|
|
||||||
@ -294,8 +294,8 @@ class Playlists(Base):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
session.query(cls)
|
session.query(cls)
|
||||||
.filter(cls.loaded.is_(False))
|
.filter(cls.loaded.is_(False))
|
||||||
.order_by(cls.last_used.desc())
|
.order_by(cls.last_used.desc())
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -306,8 +306,8 @@ class Playlists(Base):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
session.query(cls)
|
session.query(cls)
|
||||||
.filter(cls.loaded.is_(True))
|
.filter(cls.loaded.is_(True))
|
||||||
.order_by(cls.last_used.desc())
|
.order_by(cls.last_used.desc())
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def mark_open(self, session: Session) -> None:
|
def mark_open(self, session: Session) -> None:
|
||||||
@ -317,9 +317,8 @@ class Playlists(Base):
|
|||||||
self.last_used = datetime.now()
|
self.last_used = datetime.now()
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
def move_track(
|
def move_track(self, session: Session, rows: List[int],
|
||||||
self, session: Session, rows: List[int],
|
to_playlist: "Playlists") -> None:
|
||||||
to_playlist: "Playlists") -> None:
|
|
||||||
"""Move tracks to another playlist"""
|
"""Move tracks to another playlist"""
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@ -355,7 +354,7 @@ class PlaylistTracks(Base):
|
|||||||
|
|
||||||
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
id: int = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
playlist_id: int = Column(Integer, ForeignKey('playlists.id'),
|
playlist_id: int = Column(Integer, ForeignKey('playlists.id'),
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
track_id: int = Column(Integer, ForeignKey('tracks.id'), primary_key=True)
|
track_id: int = Column(Integer, ForeignKey('tracks.id'), primary_key=True)
|
||||||
row: int = Column(Integer, nullable=False)
|
row: int = Column(Integer, nullable=False)
|
||||||
tracks: RelationshipProperty = relationship("Tracks")
|
tracks: RelationshipProperty = relationship("Tracks")
|
||||||
@ -368,10 +367,6 @@ class PlaylistTracks(Base):
|
|||||||
cascade="all, delete-orphan"
|
cascade="all, delete-orphan"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Ensure row numbers are unique within each playlist
|
|
||||||
__table_args__ = (UniqueConstraint
|
|
||||||
('row', 'playlist_id', name="uniquerow"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, session: Session, playlist_id: int, track_id: int,
|
self, session: Session, playlist_id: int, track_id: int,
|
||||||
@ -385,14 +380,14 @@ class PlaylistTracks(Base):
|
|||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def next_free_row(session: Session, playlist_id: int) -> int:
|
def next_free_row(session: Session, playlist: Playlists) -> int:
|
||||||
"""Return next free row number"""
|
"""Return next free row number"""
|
||||||
|
|
||||||
row: int
|
row: int
|
||||||
|
|
||||||
last_row = session.query(
|
last_row = session.query(
|
||||||
func.max(PlaylistTracks.row)
|
func.max(PlaylistTracks.row)
|
||||||
).filter_by(playlist_id=playlist_id).first()
|
).filter_by(playlist_id=playlist.id).first()
|
||||||
# if there are no rows, the above returns (None, ) which is True
|
# if there are no rows, the above returns (None, ) which is True
|
||||||
if last_row and last_row[0] is not None:
|
if last_row and last_row[0] is not None:
|
||||||
row = last_row[0] + 1
|
row = last_row[0] + 1
|
||||||
@ -401,33 +396,6 @@ class PlaylistTracks(Base):
|
|||||||
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def move_rows(
|
|
||||||
session: Session, rows: List[int], from_playlist_id: int,
|
|
||||||
to_playlist_id: int) -> None:
|
|
||||||
"""Move rows between playlists"""
|
|
||||||
|
|
||||||
# A constraint deliberately blocks duplicate (playlist_id, row)
|
|
||||||
# entries in database; however, unallocated rows in the database
|
|
||||||
# are fine (ie, we can have rows 1, 4, 6 and no 2, 3, 5).
|
|
||||||
# Unallocated rows will be automatically removed when the
|
|
||||||
# playlist is saved.
|
|
||||||
|
|
||||||
lowest_source_row: int = min(rows)
|
|
||||||
first_destination_free_row = PlaylistTracks.next_free_row(
|
|
||||||
session, to_playlist_id)
|
|
||||||
# Calculate offset that will put the lowest row number being
|
|
||||||
# moved at the first free row in destination playlist
|
|
||||||
offset = first_destination_free_row - lowest_source_row
|
|
||||||
|
|
||||||
session.query(PlaylistTracks).filter(
|
|
||||||
PlaylistTracks.playlist_id == from_playlist_id,
|
|
||||||
PlaylistTracks.row.in_(rows)
|
|
||||||
).update({'playlist_id': to_playlist_id,
|
|
||||||
'row': PlaylistTracks.row + offset},
|
|
||||||
False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(Base):
|
class Settings(Base):
|
||||||
__tablename__ = 'settings'
|
__tablename__ = 'settings'
|
||||||
@ -476,25 +444,24 @@ class Tracks(Base):
|
|||||||
mtime: float = Column(Float, index=True)
|
mtime: float = Column(Float, index=True)
|
||||||
lastplayed: datetime = Column(DateTime, index=True, default=None)
|
lastplayed: datetime = Column(DateTime, index=True, default=None)
|
||||||
playlists: RelationshipProperty = relationship("PlaylistTracks",
|
playlists: RelationshipProperty = relationship("PlaylistTracks",
|
||||||
back_populates="tracks",
|
back_populates="tracks",
|
||||||
lazy="joined")
|
lazy="joined")
|
||||||
playdates: RelationshipProperty = relationship("Playdates",
|
playdates: RelationshipProperty = relationship("Playdates",
|
||||||
back_populates="tracks",
|
back_populates="tracks",
|
||||||
lazy="joined")
|
lazy="joined")
|
||||||
|
|
||||||
def __init__(
|
def __init__(self,
|
||||||
self,
|
session: Session,
|
||||||
session: Session,
|
path: str,
|
||||||
path: str,
|
title: Optional[str] = None,
|
||||||
title: Optional[str] = None,
|
artist: Optional[str] = None,
|
||||||
artist: Optional[str] = None,
|
duration: Optional[int] = None,
|
||||||
duration: Optional[int] = None,
|
start_gap: Optional[int] = None,
|
||||||
start_gap: Optional[int] = None,
|
fade_at: Optional[int] = None,
|
||||||
fade_at: Optional[int] = None,
|
silence_at: Optional[int] = None,
|
||||||
silence_at: Optional[int] = None,
|
mtime: Optional[float] = None,
|
||||||
mtime: Optional[float] = None,
|
lastplayed: Optional[datetime] = None,
|
||||||
lastplayed: Optional[datetime] = None,
|
) -> None:
|
||||||
) -> None:
|
|
||||||
self.path = path
|
self.path = path
|
||||||
self.title = title
|
self.title = title
|
||||||
self.artist = artist
|
self.artist = artist
|
||||||
@ -589,10 +556,10 @@ class Tracks(Base):
|
|||||||
audio: AudioSegment = get_audio_segment(self.path)
|
audio: AudioSegment = get_audio_segment(self.path)
|
||||||
self.duration = len(audio)
|
self.duration = len(audio)
|
||||||
self.fade_at = round(fade_point(audio) / 1000,
|
self.fade_at = round(fade_point(audio) / 1000,
|
||||||
Config.MILLISECOND_SIGFIGS) * 1000
|
Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
self.mtime = os.path.getmtime(self.path)
|
self.mtime = os.path.getmtime(self.path)
|
||||||
self.silence_at = round(trailing_silence(audio) / 1000,
|
self.silence_at = round(trailing_silence(audio) / 1000,
|
||||||
Config.MILLISECOND_SIGFIGS) * 1000
|
Config.MILLISECOND_SIGFIGS) * 1000
|
||||||
self.start_gap = leading_silence(audio)
|
self.start_gap = leading_silence(audio)
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.flush()
|
session.flush()
|
||||||
@ -614,16 +581,16 @@ class Tracks(Base):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
session.query(cls)
|
session.query(cls)
|
||||||
.filter(cls.artist.ilike(f"%{text}%"))
|
.filter(cls.artist.ilike(f"%{text}%"))
|
||||||
.order_by(cls.title)
|
.order_by(cls.title)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_titles(cls, session: Session, text: str) -> List["Tracks"]:
|
def search_titles(cls, session: Session, text: str) -> List["Tracks"]:
|
||||||
return (
|
return (
|
||||||
session.query(cls)
|
session.query(cls)
|
||||||
.filter(cls.title.ilike(f"%{text}%"))
|
.filter(cls.title.ilike(f"%{text}%"))
|
||||||
.order_by(cls.title)
|
.order_by(cls.title)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -424,8 +424,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
# Update database for both source and destination playlists
|
# Update database for both source and destination playlists
|
||||||
rows = visible_tab.get_selected_rows()
|
rows = visible_tab.get_selected_rows()
|
||||||
PlaylistTracks.move_rows(session, rows, source_playlist.id,
|
source_playlist.move_track(session, rows, destination_playlist)
|
||||||
destination_playlist.id)
|
|
||||||
|
|
||||||
# Update destination playlist_tab if visible (if not visible, it
|
# Update destination playlist_tab if visible (if not visible, it
|
||||||
# will be re-populated when it is opened)
|
# will be re-populated when it is opened)
|
||||||
@ -595,10 +594,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
dlg = SelectPlaylistDialog(self, playlists=playlists,
|
dlg = SelectPlaylistDialog(self, playlists=playlists,
|
||||||
session=session)
|
session=session)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
playlist = dlg.playlist
|
if dlg.plid:
|
||||||
if playlist:
|
p = Playlists.get_by_id(session=session, playlist_id=dlg.plid)
|
||||||
playlist.mark_open(session)
|
p.mark_open(session)
|
||||||
self.create_playlist_tab(session, playlist)
|
self.create_playlist_tab(session, p)
|
||||||
|
|
||||||
def select_next_row(self) -> None:
|
def select_next_row(self) -> None:
|
||||||
"""Select next or first row in playlist"""
|
"""Select next or first row in playlist"""
|
||||||
@ -972,6 +971,11 @@ class SelectPlaylistDialog(QDialog):
|
|||||||
height = record.f_int or 600
|
height = record.f_int or 600
|
||||||
self.resize(width, height)
|
self.resize(width, height)
|
||||||
|
|
||||||
|
# for (plid, plname) in [(a.id, a.name) for a in playlists]:
|
||||||
|
# p = QListWidgetItem()
|
||||||
|
# p.setText(plname)
|
||||||
|
# p.setData(Qt.UserRole, plid)
|
||||||
|
# self.ui.lstPlaylists.addItem(p)
|
||||||
for playlist in playlists:
|
for playlist in playlists:
|
||||||
p = QListWidgetItem()
|
p = QListWidgetItem()
|
||||||
p.setText(playlist.name)
|
p.setText(playlist.name)
|
||||||
|
|||||||
@ -136,7 +136,6 @@ class PlaylistTab(QTableWidget):
|
|||||||
self.itemSelectionChanged.connect(self._select_event)
|
self.itemSelectionChanged.connect(self._select_event)
|
||||||
|
|
||||||
self.editing_cell: bool = False
|
self.editing_cell: bool = False
|
||||||
self.selecting_in_progress = False
|
|
||||||
self.cellChanged.connect(self._cell_changed)
|
self.cellChanged.connect(self._cell_changed)
|
||||||
self.doubleClicked.connect(self._edit_cell)
|
self.doubleClicked.connect(self._edit_cell)
|
||||||
self.cellEditingStarted.connect(self._cell_edit_started)
|
self.cellEditingStarted.connect(self._cell_edit_started)
|
||||||
@ -391,13 +390,8 @@ class PlaylistTab(QTableWidget):
|
|||||||
# Row number will change as we delete rows so remove them in
|
# Row number will change as we delete rows so remove them in
|
||||||
# reverse order.
|
# reverse order.
|
||||||
|
|
||||||
try:
|
for row in sorted(rows, reverse=True):
|
||||||
self.selecting_in_progress = True
|
self.removeRow(row)
|
||||||
for row in sorted(rows, reverse=True):
|
|
||||||
self.removeRow(row)
|
|
||||||
finally:
|
|
||||||
self.selecting_in_progress = False
|
|
||||||
self._select_event()
|
|
||||||
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
self.save_playlist(session)
|
self.save_playlist(session)
|
||||||
@ -605,12 +599,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
def select_played_tracks(self) -> None:
|
def select_played_tracks(self) -> None:
|
||||||
"""Select all played tracks in playlist"""
|
"""Select all played tracks in playlist"""
|
||||||
|
|
||||||
try:
|
self._select_tracks(played=True)
|
||||||
self.selecting_in_progress = True
|
|
||||||
self._select_tracks(played=True)
|
|
||||||
finally:
|
|
||||||
self.selecting_in_progress = False
|
|
||||||
self._select_event()
|
|
||||||
|
|
||||||
def select_previous_row(self) -> None:
|
def select_previous_row(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -651,12 +640,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
def select_unplayed_tracks(self) -> None:
|
def select_unplayed_tracks(self) -> None:
|
||||||
"""Select all unplayed tracks in playlist"""
|
"""Select all unplayed tracks in playlist"""
|
||||||
|
|
||||||
try:
|
self._select_tracks(played=False)
|
||||||
self.selecting_in_progress = True
|
|
||||||
self._select_tracks(played=False)
|
|
||||||
finally:
|
|
||||||
self.selecting_in_progress = False
|
|
||||||
self._select_event()
|
|
||||||
|
|
||||||
def set_selected_as_next(self) -> None:
|
def set_selected_as_next(self) -> None:
|
||||||
"""Sets the select track as next to play"""
|
"""Sets the select track as next to play"""
|
||||||
@ -1427,16 +1411,11 @@ class PlaylistTab(QTableWidget):
|
|||||||
If multiple rows are selected, display sum of durations in status bar.
|
If multiple rows are selected, display sum of durations in status bar.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If we are in the process of selecting multiple tracks, no-op here
|
|
||||||
if self.selecting_in_progress:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get the row number of all selected items and put into a set
|
# Get the row number of all selected items and put into a set
|
||||||
# to deduplicate
|
# to deduplicate
|
||||||
sel_rows: Set[int] = set([item.row() for item in self.selectedItems()])
|
sel_rows: Set[int] = set([item.row() for item in self.selectedItems()])
|
||||||
# If no rows are selected, we have nothing to do
|
# If no rows are selected, we have nothing to do
|
||||||
if len(sel_rows) == 0:
|
if len(sel_rows) == 0:
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
notes_rows: Set[int] = set(self._get_notes_rows())
|
notes_rows: Set[int] = set(self._get_notes_rows())
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
"""Add constraint to playlist_tracks
|
|
||||||
|
|
||||||
Revision ID: 1c4048efee96
|
|
||||||
Revises: 52cbded98e7c
|
|
||||||
Create Date: 2022-03-29 19:26:27.378185
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import mysql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '1c4048efee96'
|
|
||||||
down_revision = '52cbded98e7c'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_unique_constraint('uniquerow', 'playlist_tracks', ['row', 'playlist_id'])
|
|
||||||
op.alter_column('playlists', 'loaded',
|
|
||||||
existing_type=mysql.TINYINT(display_width=1),
|
|
||||||
nullable=False)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.alter_column('playlists', 'loaded',
|
|
||||||
existing_type=mysql.TINYINT(display_width=1),
|
|
||||||
nullable=True)
|
|
||||||
op.drop_constraint('uniquerow', 'playlist_tracks', type_='unique')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
Loading…
Reference in New Issue
Block a user