Compare commits

..

No commits in common. "ce213221175776f84f47577da4289749d2dc9afe" and "8558de82b4854b66bbb05edc17db0a4342d55eea" have entirely different histories.

3 changed files with 71 additions and 105 deletions

View File

@ -153,30 +153,6 @@ class Notes(Base):
session.query(Notes).filter_by(id=self.id).delete()
session.flush()
@staticmethod
def max_used_row(session: Session, playlist_id: int) -> Optional[int]:
"""
Return maximum notes row for passed playlist ID or None if not notes
"""
last_row = session.query(func.max(Notes.row)).filter_by(
playlist_id=playlist_id).first()
# if there are no rows, the above returns (None, ) which is True
if last_row and last_row[0] is not None:
return last_row[0]
else:
return None
def move_row(self, session: Session, row: int, to_playlist_id: int) \
-> None:
"""
Move note to another playlist
"""
self.row = row
self.playlist_id = to_playlist_id
session.commit()
@classmethod
def get_by_id(cls, session: Session, note_id: int) -> Optional["Notes"]:
"""Return note or None"""
@ -289,8 +265,8 @@ class Playlists(Base):
If row=None, add to end of playlist
"""
if row is None:
row = self.next_free_row(session, self.id)
if not row:
row = PlaylistTracks.next_free_row(session, self.id)
PlaylistTracks(session, self.id, track_id, row)
@ -342,23 +318,17 @@ class Playlists(Base):
self.last_used = datetime.now()
session.flush()
@staticmethod
def next_free_row(session: Session, playlist_id: int) -> int:
"""Return next free row for this playlist"""
def move_track(
self, session: Session, rows: List[int],
to_playlist: "Playlists") -> None:
"""Move tracks to another playlist"""
max_notes_row = Notes.max_used_row(session, playlist_id)
max_tracks_row = PlaylistTracks.max_used_row(session, playlist_id)
for row in rows:
track = self.tracks[row]
to_playlist.add_track(session, track.id)
del self.tracks[row]
if max_notes_row is not None and max_tracks_row is not None:
return max(max_notes_row, max_tracks_row) + 1
if max_notes_row is None and max_tracks_row is None:
return 0
if max_notes_row is None:
return max_tracks_row + 1
else:
return max_notes_row + 1
session.flush()
def remove_all_tracks(self, session: Session) -> None:
"""
@ -417,30 +387,48 @@ class PlaylistTracks(Base):
session.flush()
@staticmethod
def max_used_row(session: Session, playlist_id: int) -> Optional[int]:
"""
Return highest track row number used or None if there are no
tracks
"""
def next_free_row(session: Session, playlist_id: int) -> int:
"""Return next free row number"""
row: int
last_row = session.query(
func.max(PlaylistTracks.row)
).filter_by(playlist_id=playlist_id).first()
# if there are no rows, the above returns (None, ) which is True
if last_row and last_row[0] is not None:
return last_row[0]
row = last_row[0] + 1
else:
return None
row = 0
return row
@staticmethod
def move_row(session: Session, from_row: int, from_playlist_id: int,
to_row: int, to_playlist_id: int) -> None:
"""Move row to another playlist"""
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 == from_row).update(
{'playlist_id': to_playlist_id, 'row': to_row}, False)
PlaylistTracks.row.in_(rows)
).update({'playlist_id': to_playlist_id,
'row': PlaylistTracks.row + offset},
False
)
class Settings(Base):

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
import argparse
import os.path
import psutil
import sys
import threading
@ -33,7 +34,8 @@ import helpers
import music
from config import Config
from models import (Base, Playdates, Playlists, Settings, Tracks)
from models import (Base, Playdates, Playlists, PlaylistTracks,
Settings, Tracks)
from playlists import PlaylistTab
from sqlalchemy.orm.exc import DetachedInstanceError
from ui.dlg_search_database_ui import Ui_Dialog
@ -502,8 +504,10 @@ class Window(QMainWindow, Ui_MainWindow):
return
destination_playlist = dlg.playlist
self.visible_playlist_tab().move_selected_to_playlist(
session, destination_playlist.id)
# Update database for both source and destination playlists
rows = visible_tab.get_selected_rows()
PlaylistTracks.move_rows(session, rows, source_playlist.id,
destination_playlist.id)
# Update destination playlist_tab if visible (if not visible, it
# will be re-populated when it is opened)
@ -523,6 +527,9 @@ class Window(QMainWindow, Ui_MainWindow):
destination_visible_playlist_tab.populate(
session, dlg.playlist.id)
# Update source playlist
self.visible_playlist_tab().remove_rows(rows)
def open_info_tabs(self) -> None:
"""
Ensure we have info tabs for next and current track titles
@ -1070,7 +1077,6 @@ class SelectPlaylistDialog(QDialog):
self.ui.buttonBox.accepted.connect(self.open)
self.ui.buttonBox.rejected.connect(self.close)
self.session = session
self.playlist = None
self.plid = None
record = Settings.get_int_settings(

View File

@ -30,7 +30,6 @@ from models import (
Notes,
Playdates,
Playlists,
PlaylistTracks,
Settings,
Tracks,
NoteColours
@ -151,7 +150,7 @@ class PlaylistTab(QTableWidget):
self.populate(session, self.playlist_id)
def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}"
return (f"<PlaylistTab(id={self.playlist_id}")
# ########## Events ##########
@ -315,10 +314,10 @@ class PlaylistTab(QTableWidget):
return self.selectionModel().selectedRows()[0].row()
def get_selected_rows(self) -> List[int]:
"""Return a sorted list of selected row numbers"""
"""Return a list of selected row numbers"""
rows = self.selectionModel().selectedRows()
return sorted([row.row() for row in rows])
return [row.row() for row in rows]
def get_selected_title(self) -> Optional[str]:
"""Return title of selected row or None"""
@ -399,50 +398,23 @@ class PlaylistTab(QTableWidget):
self.save_playlist(session)
self.update_display(session, clear_selection=False)
def move_selected_to_playlist(self, session: Session, playlist_id: int) \
-> None:
"""
Move selected rows and any immediately preceding notes to
other playlist
"""
def remove_rows(self, rows) -> None:
"""Remove rows passed in rows list"""
notes_rows = self._get_notes_rows()
destination_row = Playlists.next_free_row(session, playlist_id)
rows_to_remove = []
for row in self.get_selected_rows():
if row in notes_rows:
note_obj = self._get_row_notes_object(row, session)
note_obj.move_row(session, destination_row, playlist_id)
else:
# For tracks, check for a preceding notes row and move
# that as well if it exists
if row - 1 in notes_rows:
note_obj = self._get_row_notes_object(row - 1, session)
note_obj.move_row(session, destination_row, playlist_id)
destination_row += 1
rows_to_remove.append(row - 1)
# Move track
PlaylistTracks.move_row(
session, row, self.playlist_id,
destination_row, playlist_id
)
destination_row += 1
rows_to_remove.append(row)
# Remove rows. Row number will change as we delete rows so
# remove them in reverse order.
# Row number will change as we delete rows so remove them in
# reverse order.
try:
self.selecting_in_progress = True
for row in sorted(rows_to_remove, reverse=True):
for row in sorted(rows, reverse=True):
self.removeRow(row)
finally:
self.selecting_in_progress = False
self._select_event()
self.save_playlist(session)
self.update_display(session)
with Session() as session:
self.save_playlist(session)
self.update_display(session)
def play_started(self, session: Session) -> None:
"""
@ -610,7 +582,6 @@ class PlaylistTab(QTableWidget):
track_id: int = self.item(
row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
playlist.add_track(session, track_id, row)
session.commit()
def select_next_row(self) -> None:
"""
@ -860,8 +831,8 @@ class PlaylistTab(QTableWidget):
if row == next_row:
# if there's a track playing, set start time from that
if current_row is not None:
start_time = self._calculate_row_end_time(
current_row, self.current_track_start_time)
start_time = self._calculate_row_end_time(
current_row, self.current_track_start_time)
else:
# No current track to base from, but don't change
# time if it's already set
@ -885,7 +856,8 @@ class PlaylistTab(QTableWidget):
# This is a track row other than next or current
if row in played:
# Played today, so update last played column
last_playedtime = track.lastplayed
last_playedtime = Playdates.last_played(
session, track.id)
last_played_str = get_relative_date(last_playedtime)
self.item(row, self.COL_LAST_PLAYED).setText(
last_played_str)
@ -1142,11 +1114,6 @@ class PlaylistTab(QTableWidget):
return False
def _get_notes_rows(self) -> List[int]:
"""Return rows marked as notes, or None"""
return self._meta_search(RowMeta.NOTE, one=False)
def _find_next_track_row(self, starting_row: int = None) -> Optional[int]:
"""
Find next track to play. If a starting row is given, start there;
@ -1206,6 +1173,11 @@ class PlaylistTab(QTableWidget):
except ValueError:
return None
def _get_notes_rows(self) -> List[int]:
"""Return rows marked as notes, or None"""
return self._meta_search(RowMeta.NOTE, one=False)
def _get_row_duration(self, row: int) -> int:
"""Return duration associated with this row"""