1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
from glob import glob
import sys
from os import getenv
from os.path import basename, splitext
from functools import partial
from nicegui import ui
from starlette.formparsers import MultiPartParser
from solo_tool import SoloTool
from solo_tool.session_manager import loadSession, saveSession
from solo_tool import handlers
SESSION_DIR = getenv("SESSION_DIR", "/home/eddy/music/sessions")
SONG_POOL = getenv("SONG_POOL", "/home/eddy/music")
def fileName(path: str) -> str:
return basename(splitext(path)[0])
@ui.refreshable
def keyPointList(st: SoloTool) -> None:
with ui.list().props('separator'):
if st.keyPoints is not None:
for kp in st.keyPoints:
ui.item(f"{kp:0.2}", on_click=handlers.keyPointAbsolute(st, kp)).props('clickable v-ripple').classes('text-lg')
@ui.refreshable
def songList(st: SoloTool, songDrawer) -> None:
with ui.list().props('separator'):
for i, path in enumerate(st.songs):
ui.item(fileName(path), on_click=handlers.songAbsolute(st, i, lambda: songDrawer.hide())).props('clickable v-ripple')
sessions = {}
for f in glob(f"{SESSION_DIR}/*.json"):
sessionName = fileName(f)
songTool = loadSession(f, SONG_POOL)
songTool.registerKeyPointListCallback(lambda new: keyPointList.refresh())
songTool.registerSongSelectionCallback(lambda new: keyPointList.refresh())
songTool.registerSongListCallback(lambda new: songList.refresh())
sessions[sessionName] = songTool
@ui.page('/{sessionName}')
def sessionPage(sessionName: str):
if sessionName not in sessions:
return
fullscreen = ui.fullscreen()
ui.dark_mode().enable()
ui.colors(secondary='#ffc107')
ui.page_title(sessionName)
# Improved performance with large file uploads
MultiPartParser.max_file_size = 1024 * 1024 * 100 # 100 MB
st = sessions[sessionName]
# Upload song dialog
def handleFileUpload(e):
from shutil import copyfileobj
newSong = f"{SONG_POOL}/{e.name}"
with open(newSong, "wb") as f:
copyfileobj(e.content, f)
st.addSong(newSong)
with ui.dialog() as uploadSongDialog:
ui.upload(label="Upload songs", auto_upload=True, on_upload=handleFileUpload).classes('max-w-full')
# Header
with ui.header().classes('items-center justify-between'):
with ui.row().classes('items-center justify-start'):
ui.button(icon='menu', on_click=lambda: songDrawer.toggle()).props('flat round color=white')
ui.label().bind_text_from(st, 'song', lambda index: fileName(st.songs[index]) if index is not None else "Select a song").classes('text-lg')
with ui.row().classes('items-center justify-start'):
ui.button(icon='home', on_click=lambda: ui.navigate.to("/")).props('flat dense round color=white')
def save(): saveSession(st, f"{SESSION_DIR}/{sessionName}.json")
ui.button(icon='save', on_click=save).props('flat dense round color=white')
ui.button(icon='fullscreen', on_click=fullscreen.toggle).props('flat round color=white')
# Key points list
with ui.right_drawer(top_corner=True, bottom_corner=True).props('width=120 behavior=desktop'):
ui.label("Key Points").classes('text-lg')
keyPointList(st)
def addKeyPoint() -> None: st.keyPoints += [st.keyPoint]
ui.button(icon='add', on_click=addKeyPoint).props('flat round dense color=white')
# Song list
with ui.left_drawer(bottom_corner=True).props('overlay breakpoint=8000') as songDrawer:
songList(st, songDrawer)
ui.button(icon='add', on_click=uploadSongDialog.open).props('flat round dense color=white')
# Playback position
def setPosition(e) -> None: st.position = e.args
ui.slider(min=0, max=1.0, step=0.001) \
.bind_value_from(st, 'position') \
.on('change', setPosition) \
.props('thumb-size=0px track-size=16px')
# Key point position
ui.slider(min=0, max=1.0, step=0.001).bind_value(st, 'keyPoint').props('selection-color=transparent color=secondary')
# Play control
with ui.button_group().classes('w-full').style('height: 80px'):
buttonSize = "20px"
ui.button(icon='skip_previous', on_click=handlers.restartOrPreviousSong(st, 0.01)).props(f"size={buttonSize}").style('flex: 1')
ui.button(color='positive', on_click=handlers.playPause(st)).bind_icon_from(st, "playing", lambda playing: "pause" if playing else "play_arrow").props(f"size={buttonSize}").style('flex: 1')
ui.button(icon='vertical_align_bottom', on_click=handlers.positionToKeyPoint(st), color='negative').props(f"size={buttonSize}").style('flex: 2')
ui.button(icon='undo', on_click=st.jump, color='secondary').props(f"size={buttonSize}").style('flex: 2')
ui.button(icon='skip_next', on_click=handlers.songRelative(st, 1)).props(f"size={buttonSize}").style('flex: 1')
# Playback rate
with ui.row().classes('w-full justify-between no-wrap items-center'):
markerLabels = ",".join([f"{v}:'{v}x'" for v in [0.4, 0.6, 0.8, 1.0, 1.2]])
ui.slider(min=0.4, max=1.2, step=0.05).bind_value(st, 'rate').props(f'snap markers :marker-labels="{{{markerLabels}}}"').classes('q-px-md')
# Volume
with ui.row().classes('w-full justify-between no-wrap items-center'):
volumeLabels = ",".join([f"{v}:'{int(v*100)}%'" for v in [0.0, 0.25, 0.5, 0.75, 1.0, 1.25]])
ui.slider(min=0, max=1.25, step=0.01).bind_value(st, 'volume').props(f':marker-labels="{{{volumeLabels}}}"').classes('q-px-md')
@ui.page('/')
def landingPage():
ui.dark_mode().enable()
ui.page_title("Solo Tool")
# Header
with ui.header().classes('items-center'):
ui.label("Choose a session").classes('text-lg')
for name, soloTool in sessions.items():
ui.button(name, on_click=partial(ui.navigate.to, f"/{name}"))
ui.run(binding_refresh_interval=0.5, port=int(sys.argv[1]))
|