Compare commits
No commits in common. "d9a3dd0ec4afcc4c2236bd48efeb239e24564547" and "346509f6cad9c68404a901e4a49be3361d2d6380" have entirely different histories.
d9a3dd0ec4
...
346509f6ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,4 +14,3 @@ StudioPlaylist.png
|
|||||||
tmp/
|
tmp/
|
||||||
.coverage
|
.coverage
|
||||||
profile_output*
|
profile_output*
|
||||||
kae.py
|
|
||||||
|
|||||||
@ -316,55 +316,43 @@ def move_rows_to_playlist(
|
|||||||
Move rows between playlists.
|
Move rows between playlists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.debug(
|
|
||||||
f"move_rows_to_playlist({from_rows=}, {from_playlist_id=}, {to_row=}, {to_playlist_id=})"
|
|
||||||
)
|
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
# Sanity check row numbers
|
# Prepare desination playlist
|
||||||
_check_playlist_integrity(session, from_playlist_id, fix=False)
|
# Find last used row
|
||||||
_check_playlist_integrity(session, to_playlist_id, fix=False)
|
last_row = session.execute(
|
||||||
|
select(func.max(PlaylistRows.row_number)).where(
|
||||||
# Check there are no playlist rows with playlist_id == PENDING_MOVE
|
PlaylistRows.playlist_id == to_playlist_id
|
||||||
pending_move_rows = get_playlist_rows(Config.PLAYLIST_PENDING_MOVE)
|
)
|
||||||
if pending_move_rows:
|
).scalar_one()
|
||||||
raise ApplicationError(f"move_rows_to_playlist: {pending_move_rows=}")
|
if last_row is None:
|
||||||
|
last_row = -1
|
||||||
# Put rows to be moved into PENDING_MOVE playlist
|
# Make room in destination
|
||||||
session.execute(
|
if to_row <= last_row:
|
||||||
|
_move_rows(session, to_playlist_id, to_row, len(from_rows))
|
||||||
|
# Move rows
|
||||||
|
row_offset = to_row - min(from_rows)
|
||||||
|
stmt = (
|
||||||
update(PlaylistRows)
|
update(PlaylistRows)
|
||||||
.where(
|
.where(
|
||||||
PlaylistRows.playlist_id == from_playlist_id,
|
PlaylistRows.playlist_id == from_playlist_id,
|
||||||
PlaylistRows.row_number.in_(from_rows),
|
PlaylistRows.row_number.in_(from_rows),
|
||||||
)
|
)
|
||||||
.values(playlist_id=Config.PLAYLIST_PENDING_MOVE)
|
.values(
|
||||||
|
playlist_id=to_playlist_id,
|
||||||
|
row_number=PlaylistRows.row_number + row_offset,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
session.execute(stmt)
|
||||||
|
# Remove gaps in source
|
||||||
|
_move_rows(
|
||||||
|
session=session,
|
||||||
|
playlist_id=from_playlist_id,
|
||||||
|
starting_row=max(from_rows) + 1,
|
||||||
|
move_by=(len(from_rows) * -1),
|
||||||
|
)
|
||||||
|
# Commit changes
|
||||||
session.commit()
|
session.commit()
|
||||||
|
# Sanity check
|
||||||
# Resequence remaining row numbers
|
|
||||||
_check_playlist_integrity(session, from_playlist_id, fix=True)
|
|
||||||
|
|
||||||
# Make space for moved rows.
|
|
||||||
_move_rows(session, to_playlist_id, to_row, len(from_rows))
|
|
||||||
|
|
||||||
# Move the PENDING_MOVE rows back and fixup row numbers
|
|
||||||
update_list: list[dict[str, int]] = []
|
|
||||||
next_row = to_row
|
|
||||||
# PLAYLIST_PENDING_MOVE may have gaps so don't check it
|
|
||||||
for row_to_move in get_playlist_rows(
|
|
||||||
Config.PLAYLIST_PENDING_MOVE, check_playlist_itegrity=False
|
|
||||||
):
|
|
||||||
update_list.append(
|
|
||||||
{"id": row_to_move.playlistrow_id, "row_number": next_row}
|
|
||||||
)
|
|
||||||
update_list.append(
|
|
||||||
{"id": row_to_move.playlistrow_id, "playlist_id": to_playlist_id}
|
|
||||||
)
|
|
||||||
next_row += 1
|
|
||||||
session.execute(update(PlaylistRows), update_list)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
# Sanity check row numbers
|
|
||||||
_check_playlist_integrity(session, from_playlist_id, fix=False)
|
_check_playlist_integrity(session, from_playlist_id, fix=False)
|
||||||
_check_playlist_integrity(session, to_playlist_id, fix=False)
|
_check_playlist_integrity(session, to_playlist_id, fix=False)
|
||||||
|
|
||||||
@ -428,16 +416,9 @@ def move_rows_within_playlist(
|
|||||||
# Move the PENDING_MOVE rows back and fixup row numbers
|
# Move the PENDING_MOVE rows back and fixup row numbers
|
||||||
update_list: list[dict[str, int]] = []
|
update_list: list[dict[str, int]] = []
|
||||||
next_row = space_row
|
next_row = space_row
|
||||||
# PLAYLIST_PENDING_MOVE may have gaps so don't check it
|
for row_to_move in get_playlist_rows(Config.PLAYLIST_PENDING_MOVE):
|
||||||
for row_to_move in get_playlist_rows(
|
update_list.append({"id": row_to_move.playlistrow_id, "row_number": next_row})
|
||||||
Config.PLAYLIST_PENDING_MOVE, check_playlist_itegrity=False
|
update_list.append({"id": row_to_move.playlistrow_id, "playlist_id": playlist_id})
|
||||||
):
|
|
||||||
update_list.append(
|
|
||||||
{"id": row_to_move.playlistrow_id, "row_number": next_row}
|
|
||||||
)
|
|
||||||
update_list.append(
|
|
||||||
{"id": row_to_move.playlistrow_id, "playlist_id": playlist_id}
|
|
||||||
)
|
|
||||||
next_row += 1
|
next_row += 1
|
||||||
session.execute(update(PlaylistRows), update_list)
|
session.execute(update(PlaylistRows), update_list)
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -573,9 +554,7 @@ def get_playlist_row(playlistrow_id: int) -> PlaylistRowDTO | None:
|
|||||||
return dto
|
return dto
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_rows(
|
def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]:
|
||||||
playlist_id: int, check_playlist_itegrity=True
|
|
||||||
) -> list[PlaylistRowDTO]:
|
|
||||||
# Alias PlaydatesTable for subquery
|
# Alias PlaydatesTable for subquery
|
||||||
LatestPlaydate = aliased(Playdates)
|
LatestPlaydate = aliased(Playdates)
|
||||||
|
|
||||||
@ -618,10 +597,7 @@ def get_playlist_rows(
|
|||||||
results = session.execute(stmt).all()
|
results = session.execute(stmt).all()
|
||||||
# Sanity check
|
# Sanity check
|
||||||
# TODO: would be good to be confident at removing this
|
# TODO: would be good to be confident at removing this
|
||||||
if check_playlist_itegrity:
|
_check_playlist_integrity(session=session, playlist_rows=results, fix=False)
|
||||||
_check_playlist_integrity(
|
|
||||||
session=session, playlist_id=playlist_id, fix=False
|
|
||||||
)
|
|
||||||
|
|
||||||
dto_list = []
|
dto_list = []
|
||||||
for row in results:
|
for row in results:
|
||||||
|
|||||||
166
kae.py
Executable file
166
kae.py
Executable file
@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import datetime as dt
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QApplication, QDialog, QLabel, QLineEdit, QListWidget,
|
||||||
|
QVBoxLayout, QHBoxLayout, QPushButton, QListWidgetItem
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TrackDTO:
|
||||||
|
track_id: int
|
||||||
|
artist: str
|
||||||
|
bitrate: int
|
||||||
|
duration: int # milliseconds
|
||||||
|
fade_at: int
|
||||||
|
intro: int | None
|
||||||
|
path: str
|
||||||
|
silence_at: int
|
||||||
|
start_gap: int
|
||||||
|
title: str
|
||||||
|
lastplayed: dt.datetime | None
|
||||||
|
|
||||||
|
# Placeholder external function to simulate search
|
||||||
|
def search_titles(query: str) -> list[TrackDTO]:
|
||||||
|
now = dt.datetime.now()
|
||||||
|
dummy_tracks = [
|
||||||
|
TrackDTO(1, "Artist A", 320, 210000, 0, None, "", 0, 0, "Title One", now - dt.timedelta(days=2)),
|
||||||
|
TrackDTO(2, "Artist B", 256, 185000, 0, None, "", 0, 0, "Another Title", now - dt.timedelta(days=30)),
|
||||||
|
TrackDTO(3, "Artist C", 320, 240000, 0, None, "", 0, 0, "More Music", None),
|
||||||
|
]
|
||||||
|
return [t for t in dummy_tracks if query.lower() in t.title.lower()]
|
||||||
|
|
||||||
|
def format_duration(ms: int) -> str:
|
||||||
|
minutes, seconds = divmod(ms // 1000, 60)
|
||||||
|
return f"{minutes}:{seconds:02d}"
|
||||||
|
|
||||||
|
def friendly_last_played(lastplayed: dt.datetime | None) -> str:
|
||||||
|
if lastplayed is None:
|
||||||
|
return "(Never)"
|
||||||
|
now = dt.datetime.now()
|
||||||
|
delta = now - lastplayed
|
||||||
|
days = delta.days
|
||||||
|
|
||||||
|
if days == 0:
|
||||||
|
return "(Today)"
|
||||||
|
elif days == 1:
|
||||||
|
return "(Yesterday)"
|
||||||
|
|
||||||
|
years, days_remain = divmod(days, 365)
|
||||||
|
months, days_final = divmod(days_remain, 30)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if years:
|
||||||
|
parts.append(f"{years}y")
|
||||||
|
if months:
|
||||||
|
parts.append(f"{months}m")
|
||||||
|
if days_final:
|
||||||
|
parts.append(f"{days_final}d")
|
||||||
|
formatted = " ".join(parts)
|
||||||
|
return f"({formatted} ago)"
|
||||||
|
|
||||||
|
class TrackInsertDialog(QDialog):
|
||||||
|
signal_insert_track = pyqtSignal(int, str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Insert Track")
|
||||||
|
|
||||||
|
# Title input on one line
|
||||||
|
self.title_label = QLabel("Title:")
|
||||||
|
self.title_edit = QLineEdit()
|
||||||
|
self.title_edit.textChanged.connect(self.update_list)
|
||||||
|
|
||||||
|
title_layout = QHBoxLayout()
|
||||||
|
title_layout.addWidget(self.title_label)
|
||||||
|
title_layout.addWidget(self.title_edit)
|
||||||
|
|
||||||
|
# Track list
|
||||||
|
self.track_list = QListWidget()
|
||||||
|
|
||||||
|
# Note input on one line
|
||||||
|
self.note_label = QLabel("Note:")
|
||||||
|
self.note_edit = QLineEdit()
|
||||||
|
|
||||||
|
note_layout = QHBoxLayout()
|
||||||
|
note_layout.addWidget(self.note_label)
|
||||||
|
note_layout.addWidget(self.note_edit)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
self.add_btn = QPushButton("Add")
|
||||||
|
self.add_close_btn = QPushButton("Add and close")
|
||||||
|
self.close_btn = QPushButton("Close")
|
||||||
|
|
||||||
|
self.add_btn.clicked.connect(self.add_clicked)
|
||||||
|
self.add_close_btn.clicked.connect(self.add_and_close_clicked)
|
||||||
|
self.close_btn.clicked.connect(self.close)
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
btn_layout.addWidget(self.add_btn)
|
||||||
|
btn_layout.addWidget(self.add_close_btn)
|
||||||
|
btn_layout.addWidget(self.close_btn)
|
||||||
|
|
||||||
|
# Main layout
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addLayout(title_layout)
|
||||||
|
layout.addWidget(self.track_list)
|
||||||
|
layout.addLayout(note_layout)
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
self.resize(600, 400)
|
||||||
|
|
||||||
|
def update_list(self, text: str):
|
||||||
|
self.track_list.clear()
|
||||||
|
if text.strip() == "":
|
||||||
|
# Do not search or populate list if input is empty
|
||||||
|
return
|
||||||
|
|
||||||
|
tracks = search_titles(text)
|
||||||
|
for track in tracks:
|
||||||
|
duration_str = format_duration(track.duration)
|
||||||
|
last_played_str = friendly_last_played(track.lastplayed)
|
||||||
|
item_str = f"{track.title} - {track.artist} [{duration_str}] {last_played_str}"
|
||||||
|
item = QListWidgetItem(item_str)
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, track.track_id)
|
||||||
|
self.track_list.addItem(item)
|
||||||
|
|
||||||
|
def get_selected_track_id(self) -> int | None:
|
||||||
|
selected_items = self.track_list.selectedItems()
|
||||||
|
if selected_items:
|
||||||
|
return selected_items[0].data(Qt.ItemDataRole.UserRole)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_clicked(self):
|
||||||
|
track_id = self.get_selected_track_id()
|
||||||
|
if track_id is not None:
|
||||||
|
note_text = self.note_edit.text()
|
||||||
|
self.signal_insert_track.emit(track_id, note_text)
|
||||||
|
|
||||||
|
self.title_edit.clear()
|
||||||
|
self.note_edit.clear()
|
||||||
|
self.track_list.clear()
|
||||||
|
self.title_edit.setFocus()
|
||||||
|
|
||||||
|
def add_and_close_clicked(self):
|
||||||
|
track_id = self.get_selected_track_id()
|
||||||
|
if track_id is not None:
|
||||||
|
note_text = self.note_edit.text()
|
||||||
|
self.signal_insert_track.emit(track_id, note_text)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
# Test harness (for quick testing)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
dialog = TrackInsertDialog()
|
||||||
|
|
||||||
|
def print_inserted(track_id, note):
|
||||||
|
print(f"Inserted track ID: {track_id}, Note: '{note}'")
|
||||||
|
|
||||||
|
dialog.signal_insert_track.connect(print_inserted)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
@ -241,34 +241,3 @@ class MyTestCase(unittest.TestCase):
|
|||||||
for row in repository.get_playlist_rows(playlist.playlist_id):
|
for row in repository.get_playlist_rows(playlist.playlist_id):
|
||||||
new_order.append(int(row.note))
|
new_order.append(int(row.note))
|
||||||
assert new_order == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
assert new_order == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
def test_move_rows_to_playlist(self):
|
|
||||||
number_of_rows = 11
|
|
||||||
rows_to_move = [2, 4, 6]
|
|
||||||
to_row = 5
|
|
||||||
|
|
||||||
(playlist_src, model_src) = self.create_rows("src playlist", number_of_rows)
|
|
||||||
(playlist_dst, model_dst) = self.create_rows("dst playlist", number_of_rows)
|
|
||||||
|
|
||||||
repository.move_rows_to_playlist(
|
|
||||||
rows_to_move, playlist_src.playlist_id, to_row, playlist_dst.playlist_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check we have all rows and plr_rownums are correct
|
|
||||||
new_order_src = []
|
|
||||||
for row in repository.get_playlist_rows(playlist_src.playlist_id):
|
|
||||||
new_order_src.append(int(row.note))
|
|
||||||
assert new_order_src == [0, 1, 3, 5, 7, 8, 9, 10]
|
|
||||||
new_order_dst = []
|
|
||||||
for row in repository.get_playlist_rows(playlist_dst.playlist_id):
|
|
||||||
new_order_dst.append(int(row.note))
|
|
||||||
assert new_order_dst == [0, 1, 2, 3, 4, 2, 4, 6, 5, 6, 7, 8, 9, 10]
|
|
||||||
|
|
||||||
def test_remove_rows(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_playlist_by_id(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_settings(self):
|
|
||||||
pass
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user