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
|
from glob import glob
import sys
from os import getenv
from os.path import basename, splitext
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'):
for kp in st.keyPoints:
ui.item(f"{kp:0.2}", on_click=handlers.setKeyPoint(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.setSong(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.changeSong(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=lambda: ui.navigate.to(f"/{name}"))
ui.run(binding_refresh_interval=0.5, port=int(sys.argv[1]))
|